commit b6c2171cc237cfa3e64c5c9c6ae2827488d95bb7 Author: lijunming Date: Thu Nov 13 16:29:01 2025 +0800 初始化crush-level项目 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..2731e85 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/crushlevel-server.iml b/.idea/crushlevel-server.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/crushlevel-server.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..8e8284c --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..42288a5 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..bdb9c91 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..657b707 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/sonic-bear/.gitignore b/sonic-bear/.gitignore new file mode 100644 index 0000000..51bb6a0 --- /dev/null +++ b/sonic-bear/.gitignore @@ -0,0 +1,26 @@ +/**/rebel.xml + +#target/ +target/ + +# IDEA # +.idea/ +*.iml + +# Eclipse # +.settings/ +.classpath +.project + +# Log # +logs/ +*.log% + +#mac +.DS_Store + +#svn +.svn +#reviewboardrc +.reviewboardrc +**/rest-client.private.env.json diff --git a/sonic-bear/common/pom.xml b/sonic-bear/common/pom.xml new file mode 100644 index 0000000..7ad3e2a --- /dev/null +++ b/sonic-bear/common/pom.xml @@ -0,0 +1,54 @@ + + + + sonic-bear + com.sonic.bear + 1.0 + + 4.0.0 + + sonic-bear-common + jar + 1.0 + + + + com.sonic + common-lib + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + mysql + mysql-connector-java + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.google.guava + guava + + + com.alibaba + fastjson + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-pool2 + + + diff --git a/sonic-bear/common/src/main/java/com/sonic/bear/common/GlobalConfig.java b/sonic-bear/common/src/main/java/com/sonic/bear/common/GlobalConfig.java new file mode 100644 index 0000000..22155d0 --- /dev/null +++ b/sonic-bear/common/src/main/java/com/sonic/bear/common/GlobalConfig.java @@ -0,0 +1,106 @@ +package com.sonic.bear.common; + +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.AppRuntime; +import com.sonic.common.log.RequestLogAspect; +import com.sonic.common.rpc.HttpClient; +import com.sonic.common.rpc.RpcClient; +import com.sonic.common.rpc.RpcClientImpl; +import com.sonic.common.stat.FrequencyAlertInterceptor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.task.TaskExecutorBuilder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.stream.Collectors; + +import static org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME; + +/** + * @author coder + */ +@Slf4j +public class GlobalConfig { + @Value("${spring.application.name}") + private String appName; + @Value("${spring.application.id}") + private String appId; + + @Bean + ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() { + return new ScheduledThreadPoolExecutor(1); + } + + /** + * XXX Spring默认配置当有Executor的bean后不再装载TaskExecutor, 这里因为手动注册了 + * {@link #scheduledThreadPoolExecutor}会导致spring不再自动注册TaskExecutor, 因此需要去掉ConditionalOnMissingBean的限制. + * 参考{@link TaskExecutionAutoConfiguration#applicationTaskExecutor} + */ + @Bean(name = {APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME}) + public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { + return builder.build(); + } + + @Bean + public RequestLogAspect requestLogAspect() { + return new RequestLogAspect(); + } + + @Bean + AppRuntime appRuntime(ApplicationContext applicationContext) { + return AppRuntime.builder().appId(appId).appName(appName).applicationContext(applicationContext).build(); + } + + /** + * 用于配置接口访问频率超限告警, 接口频率配置参考application.yml中的web.frequency-alert.rules + * TODO 根据需要配置为输出到日志或者alertClient + * + * @return + */ + @Bean + public FrequencyAlertInterceptor frequencyAlertInterceptor(RuleConfig ruleConfig) { + return new FrequencyAlertInterceptor(ruleConfig.getAlertRuleMap(), + (request, frequencyAlert) -> log.warn("frequency alert, api frequency alert, url = {} alert = {}", + request.getRequestURI(), JSONObject.toJSONString(frequencyAlert))); + } + + @Bean + @Primary + public RpcClient rpcClient() { + return new RpcClientImpl(HttpClient.Config.EMPTY); + } + + @Data + @Component + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "web.frequency-alert") + public static class RuleConfig { + private List rules; + + public Map getAlertRuleMap() { + if (rules == null) { + return Collections.emptyMap(); + } + return rules.stream() + .map(e -> { + String[] split = e.split(":"); + return new AbstractMap.SimpleEntry<>(split[0], split[1]); + }).collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); + } + } +} diff --git a/sonic-bear/common/src/main/java/com/sonic/bear/common/MybatisPlusConfig.java b/sonic-bear/common/src/main/java/com/sonic/bear/common/MybatisPlusConfig.java new file mode 100644 index 0000000..7f95fa7 --- /dev/null +++ b/sonic-bear/common/src/main/java/com/sonic/bear/common/MybatisPlusConfig.java @@ -0,0 +1,19 @@ +package com.sonic.bear.common; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; + +@EnableTransactionManagement +@MapperScan("com.sonic.**.dao") +public class MybatisPlusConfig { + + @Bean + public PaginationInterceptor paginationInterceptor() { + PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); + paginationInterceptor.setDialectType("mysql"); + return paginationInterceptor; + } +} diff --git a/sonic-bear/lib/pom.xml b/sonic-bear/lib/pom.xml new file mode 100644 index 0000000..e62ce92 --- /dev/null +++ b/sonic-bear/lib/pom.xml @@ -0,0 +1,47 @@ + + + + sonic-bear + com.sonic.bear + 1.0 + + 4.0.0 + + com.sonic.bear + sonic-bear-lib + jar + 1.1-SNAPSHOT + + + + + + + + + + + + + + + + + + + + + com.sonic + common-lib + + + + com.github.ben-manes.caffeine + caffeine + + + + + \ No newline at end of file diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserLoginClient.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserLoginClient.java new file mode 100644 index 0000000..e887102 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserLoginClient.java @@ -0,0 +1,71 @@ +package com.sonic.bear.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.ImmutableMap; +import com.sonic.bear.lib.input.ThirdUserLoginInput; +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author code + */ +@Slf4j +@Service +public class UserLoginClient { + + private static final String URI_THIRD_USER_LOGIN = "/api/auth/third-user-login"; + private static final String URI_LOGOUT = "/api/auth/logout"; + private static final String URI_KICK_OUT = "/api/auth/kick-out"; + + private RpcClient rpcClient; + private String host; + + public UserLoginClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-bear-svc:8080"; + break; + case product: + default: + this.host = "http://prod-bear-svc:8080"; + } + } + + + /** + * 三方账号登录 + * @param input + * @return + */ + public Session thirdLogin(ThirdUserLoginInput input) { + return rpcClient.post(host + URI_THIRD_USER_LOGIN, input, new TypeReference>(){}); + } + + /** + * 退出登录 + * @param token + */ + public void logout(String token) { + rpcClient.post(host.concat(URI_LOGOUT), ImmutableMap.of("token", token), new TypeReference>(){}); + } + + /** + * 踢下线 + * @param userId + */ + public void kickOut(Long userId) { + rpcClient.post(host.concat(URI_KICK_OUT), ImmutableMap.of("userId", userId), new TypeReference>(){}); + } + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserNicknamePoolClient.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserNicknamePoolClient.java new file mode 100644 index 0000000..07e511f --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserNicknamePoolClient.java @@ -0,0 +1,103 @@ +package com.sonic.bear.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.ImmutableMap; +import com.sonic.bear.lib.enums.OptTypeEnum; +import com.sonic.bear.lib.enums.UserTypeEnum; +import com.sonic.bear.lib.input.UserNicknameSyncPoolInput; +import com.sonic.bear.lib.output.NicknameOutput; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author code + */ +@Slf4j +@Service +public class UserNicknamePoolClient { + + private static final String URI_USER_NICKNAME_EXIST_CHECK = "/api/user-nickname/exist-check"; + + private static final String URI_USER_NICKNAME_SYNC_POOL = "/api/user-nickname/sync-pool"; + + private static final String URI_USER_NICKNAME_BATCH_GET = "/api/user-nickname/batch-get"; + + + + private RpcClient rpcClient; + private String host; + + public UserNicknamePoolClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-bear-svc:8080"; + break; + case product: + default: + this.host = "http://prod-bear-svc:8080"; + } + } + + + /** + * 昵称重复性校验 + * @param nickname + * @return + */ + public boolean userNicknameExistCheck(String nickname) { + return rpcClient.post(host + URI_USER_NICKNAME_EXIST_CHECK, ImmutableMap.of("nickname", nickname), new TypeReference>(){}); + } + + /** + * 昵称重复性校验 + * @param exUserId + * @param nickname + * @return + */ + public boolean userNicknameExistCheck(Long exUserId, String nickname) { + return rpcClient.post(host + URI_USER_NICKNAME_EXIST_CHECK, ImmutableMap.of("exUserId", exUserId, "nickname", nickname), new TypeReference>(){}); + } + + /** + * 同步AI昵称到数据池 + * @param nickname + * @return + */ + public void syncAiNickname(Long aiUserId, String nickname, OptTypeEnum optType) { + UserNicknameSyncPoolInput input = UserNicknameSyncPoolInput.builder() + .userId(aiUserId) + .nickname(nickname) + .userType(UserTypeEnum.AI.getCode()) + .optType(optType) + .build(); + rpcClient.post(host + URI_USER_NICKNAME_SYNC_POOL, input, new TypeReference>(){}); + } + + /** + * 批量获取用户昵称 + * @param userIdList + * @return + */ + public Map batchGetNickname(List userIdList) { + Map param = new HashMap<>(1); + param.put("userIdList", userIdList); + List outputList = rpcClient.post(host + URI_USER_NICKNAME_BATCH_GET, param, new TypeReference>>(){}); + return outputList.stream().collect(HashMap::new, (m, v) -> m.put(v.getUserId(), v.getNickname()), HashMap::putAll); + } + + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserSearchClient.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserSearchClient.java new file mode 100644 index 0000000..dcf2b16 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserSearchClient.java @@ -0,0 +1,82 @@ +package com.sonic.bear.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.bear.lib.input.UserIdInput; +import com.sonic.bear.lib.input.UserIdListInput; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.bear.lib.output.UserInfoListOutput; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author code + */ +@Slf4j +@Service +public class UserSearchClient { + + private static final String URI_BATCH_GET_USER_INFO = "/api/user/batch-get-info"; + + private static final String URI_BASE_USER_INFO = "/api/user/base-user-info"; + + private RpcClient rpcClient; + private String host; + + public UserSearchClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-bear-svc:8080"; + break; + case product: + default: + this.host = "http://prod-bear-svc:8080"; + } + } + + + /** + * 批量获取用户基础信息 + * @param userIdList + * @return + */ + public Map batchGetUserInfoToMap(List userIdList) { + List list = batchGetUserInfo(userIdList); + return list.stream().collect(Collectors.toMap(UserInfoListOutput::getUserId, v -> v)); + } + + /** + * 批量获取用户基础信息 + * @param userIdList + * @return + */ + public List batchGetUserInfo(List userIdList) { + UserIdListInput input = UserIdListInput.builder().userIdList(userIdList).build(); + return rpcClient.post(host + URI_BATCH_GET_USER_INFO, input, new TypeReference>>(){}); + } + + /** + * 获取用户自己的基础信息 + * @param userId + * @return + */ + public BaseUserInfoOutput baseUserInfo(Long userId) { + UserIdInput input = UserIdInput.builder().userId(userId).build(); + return rpcClient.post(host + URI_BASE_USER_INFO, input, new TypeReference>(){}); + } + + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserSessionClient.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserSessionClient.java new file mode 100644 index 0000000..548b805 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserSessionClient.java @@ -0,0 +1,55 @@ +package com.sonic.bear.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.ImmutableMap; +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author code + */ +@Slf4j +@Service +public class UserSessionClient { + + private static final String URI_TOUCH_SESSION = "/api/auth/touch-session"; + + private RpcClient rpcClient; + private String host; + + public UserSessionClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://dev-bear.svc.cloud"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-bear-svc:8080"; + break; + case product: + default: + this.host = "http://prod-bear-svc:8080"; + } + } + + /** + * 正常返回则表示该session处于活跃状态;抛出异常表示该会话已过期。 + *

+ * LoadingCache.get,如果缓存中不存在,则调用load直接访问远程服务,并放入本地缓存。 + * 如果 load 抛出异常,get 会直接抛出 load 的异常 + *

+ * + * @param token 用户会话TOKEN + */ + public Session touchSession(final String token) { + return rpcClient.post(host + URI_TOUCH_SESSION, ImmutableMap.of("token", token), new TypeReference>(){}); + } + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserSetClient.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserSetClient.java new file mode 100644 index 0000000..2bbd64f --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/client/UserSetClient.java @@ -0,0 +1,80 @@ +package com.sonic.bear.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.bear.lib.input.CompleteUserInfoInput; +import com.sonic.bear.lib.input.EditUserInfoInput; +import com.sonic.bear.lib.input.UserIdInput; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author code + */ +@Slf4j +@Service +public class UserSetClient { + + private static final String URI_COMPLETE_USER_INFO = "/api/user/complete-user-info"; + + private static final String URI_EDIT_USER_INFO = "/api/user/edit-user-info"; + + private static final String URI_DEL_ACCOUNT = "/api/user/del-account"; + + + + private RpcClient rpcClient; + private String host; + + public UserSetClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-bear-svc:8080"; + break; + case product: + default: + this.host = "http://prod-bear-svc:8080"; + } + } + + + /** + * 完善用户基础信息 + * @param input + * @return + */ + public void completeUserInfo(CompleteUserInfoInput input) { + rpcClient.post(host + URI_COMPLETE_USER_INFO, input, new TypeReference>(){}); + } + + /** + * 编辑用户基础信息 + * @param input + * @return + */ + public void editUserInfo(EditUserInfoInput input) { + rpcClient.post(host + URI_EDIT_USER_INFO, input, new TypeReference>(){}); + } + + /** + * 编辑用户基础信息 + * @param userId + * @return + */ + public void delAccount(Long userId) { + UserIdInput input = UserIdInput.builder() + .userId(userId) + .build(); + rpcClient.post(host + URI_DEL_ACCOUNT, input, new TypeReference>(){}); + } + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/AccountTypeEnum.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/AccountTypeEnum.java new file mode 100644 index 0000000..39f2a98 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/AccountTypeEnum.java @@ -0,0 +1,36 @@ +package com.sonic.bear.lib.enums; + +import lombok.Getter; + +/** + * 账号类型 + * @author code + */ +@Getter +public enum AccountTypeEnum { + /** + * 客户用户 + */ + CUSTOMER(1), + /** + * 后台账号 + */ + SYSTEM(2), + ; + + private final Integer code; + + AccountTypeEnum(Integer code) { + this.code = code; + } + + public static AccountTypeEnum from(Integer code) { + for (AccountTypeEnum status : AccountTypeEnum.values()) { + if (code.equals(status.code)) { + return status; + } + } + + return null; + } +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/OptTypeEnum.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/OptTypeEnum.java new file mode 100644 index 0000000..aef567e --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/OptTypeEnum.java @@ -0,0 +1,7 @@ +package com.sonic.bear.lib.enums; + +public enum OptTypeEnum { + ADD, + DEL, + UPD +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/ThirdTypeEnum.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/ThirdTypeEnum.java new file mode 100644 index 0000000..8423fd9 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/ThirdTypeEnum.java @@ -0,0 +1,13 @@ +package com.sonic.bear.lib.enums; + +/** + * 三方类型枚举 + */ +public enum ThirdTypeEnum { + + DISCORD, + GOOGLE, + APPLE, + ; + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/UserTypeEnum.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/UserTypeEnum.java new file mode 100644 index 0000000..e3c3247 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/enums/UserTypeEnum.java @@ -0,0 +1,17 @@ +package com.sonic.bear.lib.enums; + +import lombok.Getter; + +@Getter +public enum UserTypeEnum { + + USER(1), + AI(2); + + private Integer code; + + UserTypeEnum(Integer code) { + this.code = code; + } + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/CompleteUserInfoInput.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/CompleteUserInfoInput.java new file mode 100644 index 0000000..6b61990 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/CompleteUserInfoInput.java @@ -0,0 +1,33 @@ +package com.sonic.bear.lib.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class CompleteUserInfoInput { + + @ApiModelProperty("用户ID") + private Long userId; + + @ApiModelProperty("headImage") + private String headImage; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("性别") + private Integer sex; + + @ApiModelProperty("生日") + private LocalDateTime birthday; + + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/EditUserInfoInput.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/EditUserInfoInput.java new file mode 100644 index 0000000..4707c6c --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/EditUserInfoInput.java @@ -0,0 +1,30 @@ +package com.sonic.bear.lib.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class EditUserInfoInput { + + @ApiModelProperty("用户ID") + private Long userId; + + @ApiModelProperty("headImage") + private String headImage; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("生日") + private LocalDateTime birthday; + + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/ThirdUserLoginInput.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/ThirdUserLoginInput.java new file mode 100644 index 0000000..e720313 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/ThirdUserLoginInput.java @@ -0,0 +1,47 @@ +package com.sonic.bear.lib.input; + +import com.sonic.bear.lib.enums.ThirdTypeEnum; +import com.sonic.common.auth.domains.AppClientEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ThirdUserLoginInput { + + @ApiModelProperty("三方ID") + @NotBlank + private String openId; + + @ApiModelProperty("三方邮箱") + private String email; + + @ApiModelProperty("三方昵称") + private String nickname; + + @ApiModelProperty("三方类型(DISCORD、GOOGLE、APPLE)") + @NotNull + private ThirdTypeEnum thirdType; + + @ApiModelProperty("客户端标识(WEB、IOS、ANDROID、ADMIN)") + @NotBlank + private AppClientEnum clientCode; + + @ApiModelProperty("设备ID") + private String deviceId; + + @ApiModelProperty("IP地址") + private String ip; + + @ApiModelProperty("UA") + private String userAgent; + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserIdInput.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserIdInput.java new file mode 100644 index 0000000..d491257 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserIdInput.java @@ -0,0 +1,21 @@ +package com.sonic.bear.lib.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UserIdInput { + + @ApiModelProperty("用户ID") + @NotNull + private Long userId; + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserIdListInput.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserIdListInput.java new file mode 100644 index 0000000..0e6add5 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserIdListInput.java @@ -0,0 +1,22 @@ +package com.sonic.bear.lib.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UserIdListInput { + + @ApiModelProperty("用户ID列表") + @NotNull + private List userIdList; + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserNicknameExistCheckInput.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserNicknameExistCheckInput.java new file mode 100644 index 0000000..d82a071 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserNicknameExistCheckInput.java @@ -0,0 +1,20 @@ +package com.sonic.bear.lib.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UserNicknameExistCheckInput { + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("需要排除的用户ID") + private Long exUserId; +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserNicknameSyncPoolInput.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserNicknameSyncPoolInput.java new file mode 100644 index 0000000..23d5f4b --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/input/UserNicknameSyncPoolInput.java @@ -0,0 +1,31 @@ +package com.sonic.bear.lib.input; + +import com.sonic.bear.lib.enums.OptTypeEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UserNicknameSyncPoolInput { + + @NotNull + @ApiModelProperty("用户ID") + private Long userId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("用户类型") + private Integer userType; + + @ApiModelProperty("操作类型") + private OptTypeEnum optType; + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/output/BaseUserInfoOutput.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/output/BaseUserInfoOutput.java new file mode 100644 index 0000000..893d070 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/output/BaseUserInfoOutput.java @@ -0,0 +1,51 @@ +package com.sonic.bear.lib.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class BaseUserInfoOutput { + + @ApiModelProperty("用户ID") + private Long userId; + + @ApiModelProperty("headImage") + private String headImage; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("性别") + private Integer sex; + + @ApiModelProperty("ID编号") + private String idCard; + + @ApiModelProperty("生日") + private LocalDateTime birthday; + + @ApiModelProperty("账号类型") + private String thirdType; + + @ApiModelProperty("账号昵称") + private String thirdNickname; + + @ApiModelProperty("账号邮箱") + private String thirdEmail; + + //=============== 开关相关字段 =============== + + @ApiModelProperty("是否需要强制完善用户基础信息") + private Boolean cpUserInfo; + + @ApiModelProperty("是否是会员") + private Boolean isMember; +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/output/NicknameOutput.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/output/NicknameOutput.java new file mode 100644 index 0000000..5901b92 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/output/NicknameOutput.java @@ -0,0 +1,30 @@ +package com.sonic.bear.lib.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author code + * @Description 用户昵称 + * @Date 2024/1/12 11:57 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NicknameOutput { + + /** + * 用户ID + */ + private Long userId; + + /** + * 昵称 + */ + private String nickname; + +} diff --git a/sonic-bear/lib/src/main/java/com/sonic/bear/lib/output/UserInfoListOutput.java b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/output/UserInfoListOutput.java new file mode 100644 index 0000000..3986dd2 --- /dev/null +++ b/sonic-bear/lib/src/main/java/com/sonic/bear/lib/output/UserInfoListOutput.java @@ -0,0 +1,30 @@ +package com.sonic.bear.lib.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UserInfoListOutput { + + @ApiModelProperty("用户ID") + private Long userId; + + @ApiModelProperty("idCard") + private String idCard; + + @ApiModelProperty("headImage") + private String headImage; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("性别") + private Integer sex; + +} diff --git a/sonic-bear/pom.xml b/sonic-bear/pom.xml new file mode 100644 index 0000000..4e9b009 --- /dev/null +++ b/sonic-bear/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + com.sonic + common-parent-pom + 1.0 + + + sonic-bear + + com.sonic.bear + pom + 1.0 + + + + 1.0.6 + 1.0 + + + + + + com.sonic + common-lib + ${common-lib.version} + + + com.sonic + dao-support-lib + ${dao-support-lib.version} + + + + + + + org.projectlombok + lombok + + + + + common + server + lib + + + + + + + + + + + + + + diff --git a/sonic-bear/server/pom.xml b/sonic-bear/server/pom.xml new file mode 100644 index 0000000..477b655 --- /dev/null +++ b/sonic-bear/server/pom.xml @@ -0,0 +1,121 @@ + + + + sonic-bear + com.sonic.bear + 1.0 + + 4.0.0 + + sonic-bear-server + jar + + + + com.sonic.bear + sonic-bear-common + 1.0 + + + + com.sonic.sdk + sonic-common-api + 1.0.1 + + + + com.sonic + dao-support-lib + 1.0 + + + + com.sonic.bear + sonic-bear-lib + 1.1-SNAPSHOT + + + + com.sonic.pigeon + sonic-pigeon-lib + 1.1-SNAPSHOT + + + + com.sonic.lion + sonic-lion-lib + 1.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + it.ozimov + embedded-redis + + + + + org.redisson + redisson-spring-boot-starter + 3.13.1 + + + + + org.springframework.amqp + spring-amqp + + + org.springframework.amqp + spring-rabbit + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + + + com.h2database + h2 + + + + com.github.ben-manes.caffeine + caffeine + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pl.project13.maven + git-commit-id-plugin + + + false + false + + + + + diff --git a/sonic-bear/server/src/main/java/com/baomidou/mybatisplus/core/toolkit/Sequence.java b/sonic-bear/server/src/main/java/com/baomidou/mybatisplus/core/toolkit/Sequence.java new file mode 100644 index 0000000..d41c757 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/baomidou/mybatisplus/core/toolkit/Sequence.java @@ -0,0 +1,175 @@ +package com.baomidou.mybatisplus.core.toolkit; + +import lombok.extern.slf4j.Slf4j; + +import java.net.InetAddress; + +/** + * 原始链接 https://segmentfault.com/a/1190000020835840 + * 雪花算法分布式唯一ID生成器
+ * 每个机器号最高支持每秒‭65535个序列, 当秒序列不足时启用备份机器号, 若备份机器也不足时借用备份机器下一秒可用序列
+ * 53 bits 趋势自增ID结构如下: + *

+ * |00000000|00011111|11111111|11111111|11111111|11111111|11111111|11111111| + * |-----------|##########32bit 秒级时间戳##########|-----|-----------------| + * |--------------------------------------5bit机器位|xxxxx|-----------------| + * |-----------------------------------------16bit自增序列|xxxxxxxx|xxxxxxxx| + * + * @author: + * @date: 2021-12-30 + **/ +@Slf4j +public class Sequence { + /** + * 初始偏移时间戳 + */ + private final long OFFSET = 1546300800L; + + /** + * 机器id (0~15 保留 16~31作为备份机器) + */ + private long WORKER_ID; + /** + * 机器id所占位数 (5bit, 支持最大机器数 2^5 = 32) + */ + private final long WORKER_ID_BITS = 5L; + /** + * 自增序列所占位数 (16bit, 支持最大每秒生成 2^16 = ‭65536‬) + */ + private final long SEQUENCE_ID_BITS = 16L; + /** + * 机器id偏移位数 + */ + private final long WORKER_SHIFT_BITS = SEQUENCE_ID_BITS; + /** + * 自增序列偏移位数 + */ + private final long OFFSET_SHIFT_BITS = SEQUENCE_ID_BITS + WORKER_ID_BITS; + /** + * 机器标识最大值 (2^5 / 2 - 1 = 15) + */ + private final long WORKER_ID_MAX = ((1 << WORKER_ID_BITS) - 1) >> 1; + /** + * 备份机器ID开始位置 (2^5 / 2 = 16) + */ + private final long BACK_WORKER_ID_BEGIN = (1 << WORKER_ID_BITS) >> 1; + /** + * 自增序列最大值 (2^16 - 1 = ‭65535) + */ + private final long SEQUENCE_MAX = (1 << SEQUENCE_ID_BITS) - 1; + /** + * 发生时间回拨时容忍的最大回拨时间 (秒) + */ + private final long BACK_TIME_MAX = 1L; + + /** + * 上次生成ID的时间戳 (秒) + */ + private long lastTimestamp = 0L; + /** + * 当前秒内序列 (2^16) + */ + private long sequence = 0L; + /** + * 备份机器上次生成ID的时间戳 (秒) + */ + private long lastTimestampBak = 0L; + /** + * 备份机器当前秒内序列 (2^16) + */ + private long sequenceBak = 0L; + + public static int WORK_ID = 0; + + /** + * 使用ip第三位作为工作线程ID + */ + static { + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + String last = inetAddress.getHostAddress().split("\\.")[3]; + WORK_ID = Integer.valueOf(last) % 15; + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 私有构造函数禁止外部访问 + */ + public Sequence() { + WORKER_ID = WORK_ID; + } + + public Sequence(long workerId, long datacenterId) { + WORKER_ID = WORK_ID; + } + + /** + * 获取自增序列 + * + * @return long + */ + public long nextId() { + return nextId(SystemClock.now() / 1000); + } + + /** + * 主机器自增序列 + * + * @param timestamp 当前Unix时间戳 + * @return long + */ + private synchronized long nextId(long timestamp) { + // 时钟回拨检查 + if (timestamp < lastTimestamp) { + // 发生时钟回拨 + log.warn("时钟回拨, 启用备份机器ID: now: [{}] last: [{}]", timestamp, lastTimestamp); + return nextIdBackup(timestamp); + } + + // 开始下一秒 + if (timestamp != lastTimestamp) { + lastTimestamp = timestamp; + sequence = 0L; + } + if (0L == (++sequence & SEQUENCE_MAX)) { + // 秒内序列用尽 + log.warn("秒内[{}]序列用尽, 启用备份机器ID序列", timestamp); + sequence--; + return nextIdBackup(timestamp); + } + + return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | (WORKER_ID << WORKER_SHIFT_BITS) | sequence; + } + + /** + * 备份机器自增序列 + * + * @param timestamp timestamp 当前Unix时间戳 + * @return long + */ + private long nextIdBackup(long timestamp) { + if (timestamp < lastTimestampBak) { + if (lastTimestampBak - SystemClock.now() / 1000 <= BACK_TIME_MAX) { + timestamp = lastTimestampBak; + } else { + throw new RuntimeException(String.format("时钟回拨: now: [%d] last: [%d]", timestamp, lastTimestampBak)); + } + } + + if (timestamp != lastTimestampBak) { + lastTimestampBak = timestamp; + sequenceBak = 0L; + } + + if (0L == (++sequenceBak & SEQUENCE_MAX)) { + // 秒内序列用尽 + log.warn("秒内[{}]序列用尽, 备份机器ID借取下一秒序列", timestamp); + return nextIdBackup(timestamp + 1); + } + + return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | ((WORKER_ID ^ BACK_WORKER_ID_BEGIN) << WORKER_SHIFT_BITS) | sequenceBak; + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/MainApplication.java b/sonic-bear/server/src/main/java/com/sonic/bear/MainApplication.java new file mode 100644 index 0000000..81893ee --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/MainApplication.java @@ -0,0 +1,20 @@ +package com.sonic.bear; + +import com.sonic.sdk.api.annotation.EnableGatWayAuthScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * @author coder + */ +@EnableSwagger2 +@ComponentScan(value = {"com.sonic"}) +@SpringBootApplication +@EnableGatWayAuthScan(basePackages = "com.sonic.bear.controller") +public class MainApplication { + public static void main(String[] args) { + SpringApplication.run(MainApplication.class, args); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/config/Config.java b/sonic-bear/server/src/main/java/com/sonic/bear/config/Config.java new file mode 100644 index 0000000..f04630a --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/config/Config.java @@ -0,0 +1,48 @@ +package com.sonic.bear.config; + +import com.sonic.bear.common.GlobalConfig; +import com.sonic.bear.common.MybatisPlusConfig; +import com.sonic.common.AppRuntime; +import com.sonic.common.config.DefaultWebMvcConfig; +import com.sonic.common.exception.AbstractExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; + +import java.util.function.Consumer; + +/** + * Server服务唯一的配置入口 + * TODO: Import的各种Config,按需使用(例如,如果不需要mysql,kafka消息,redis等,可以去掉对应的Config) + * + * @author coder + */ +@Slf4j +@Configuration +@Import({GlobalConfig.class, DefaultWebMvcConfig.class, MybatisPlusConfig.class, RedisConfig.class, + EventConfig.class, SwaggerConfig.class}) +public class Config { + + /** + * XXX: ExceptionHandlerConfig 应该在 DefaultWebMvcConfig 前被初始. 否则 DefaultWebMvcConfig 的 exceptionHandlerHook 会生效 + */ + static class ExceptionHandlerConfig { + @Bean + @Profile({"!unittest && !local"}) + AbstractExceptionHandler.ExceptionHandlerHook exceptionHandlerHook(AppRuntime appRuntime) { + return new AbstractExceptionHandler.ExceptionHandlerHook() { + @Override + public String getAppId() { + return appRuntime.getAppId(); + } + + @Override + public Consumer getExceptionConsumer() { + return context -> log.error("未捕获内部异常, logText: {}", context.dumpRequest(), context.getException()); + } + }; + } + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/config/EventConfig.java b/sonic-bear/server/src/main/java/com/sonic/bear/config/EventConfig.java new file mode 100644 index 0000000..d136e08 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/config/EventConfig.java @@ -0,0 +1,147 @@ +package com.sonic.bear.config; + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.ImmutableMap; +import com.rabbitmq.client.Channel; +import com.sonic.common.AppRuntime; +import com.sonic.common.event.*; +import com.sonic.common.utils.LogUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.annotation.RabbitListeners; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Payload; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * @author coder + */ +@Slf4j +public class EventConfig { + + /** TODO: 定义 Event.BuildInScene */ + public final static String DEFAULT_SCENE = Event.BuildInScene.SONIC.getCode(); + /** TODO: DEFAULT_SCENE + "_" + appName */ + public final static String DEFAULT_MODULE = DEFAULT_SCENE + "_" + "sonic"; + + @Value("${mq.exchange}") + private String mqExchange; + @Value("${mq.default.queue}") + private String defaultQueue; + @Value("${mq.default.routing-key}") + private String defaultRoutingKey; + + @Value("${mq.user-created.queue}") + private String userCreatedQueue; + @Value("${mq.user-created.routing-key}") + private String userCreatedRoutingKey; + + @Configuration + @Profile("!unittest") + static class Listener { + @Autowired + EventConsumer eventConsumer; + + @RabbitListeners({ + @RabbitListener(queues = {"${mq.default.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.user-created.queue}"}, concurrency = "2"), + }) + public void process(@Payload Message message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) { + try { + LogUtils.setTraceId(); + byte[] msgBody = message.getBody(); + String contentEncoding = message.getMessageProperties().getContentEncoding(); + String bodyStr = new String(msgBody, Charset.forName(ObjectUtils.defaultIfNull(contentEncoding, "UTF-8"))); + MessageProperties pro = message.getMessageProperties(); + if (log.isDebugEnabled()) { + log.info("receive msg routingKey:{}->consumerQueue:{}->redelivered:{}->body:{}", + pro.getReceivedRoutingKey(), pro.getConsumerQueue(), pro.getRedelivered(), bodyStr); + } + eventConsumer.onEvent(bodyStr, ImmutableMap.of("record", JSON.toJSONString(message))); + + //消息确认,multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息 + channel.basicAck(deliveryTag, false); + } catch (Exception e) { + log.error("===> mq handler error, message: {}", JSON.toJSONString(message), e); + //消息拒绝,//requeue=true,表示将消息重新放入到队列中,false:表示直接从队列中删除,此时和basicAck(long deliveryTag, false)的效果一样 + try { + channel.basicReject(deliveryTag,true); + } catch (IOException ioException) { + log.error("channel.basicReject error. message: {}, deliveryTag: {}", JSON.toJSONString(message), deliveryTag, ioException); + } + } finally { + LogUtils.removeTraceId(); + } + } + + } + + @Bean + EventProducer eventProducer(AppRuntime appRuntime, RabbitTemplate rabbitTemplate, TaskExecutor taskExecutor) { + BiConsumer callback = (event, meta) -> {}; + return new RabbitmqEventProducer(rabbitTemplate, DEFAULT_MODULE, DEFAULT_SCENE, appRuntime.getAppId(), + RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, defaultRoutingKey), taskExecutor, callback); + } + + @Bean + EventConsumer eventConsumer(AppRuntime appRuntime, EventHandlerRepository eventHandlerRepository) { + Consumer callback = (eventWrapper) -> {}; + return new DefaultEventConsumer(appRuntime.getAppName(), eventHandlerRepository, callback); + } + + @Bean + EventHandlerRepository eventHandlerRepository() { + return new EventHandlerRepository((ex, logText) -> log.error("MQ,handle error {}", logText, ex)); + } + + @Bean + public DirectExchange messageServerExchange(){ + return new DirectExchange(mqExchange); + } + + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta userCreatedMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, userCreatedRoutingKey); + } + + @Bean + public Binding bindingDefaultQueueExchange(DirectExchange exchange, Queue defaultQueue) { + return bindingExchange(exchange, defaultQueue, defaultRoutingKey); + } + + @Bean + public Binding bindingUserCreatedQueueExchange(DirectExchange exchange, Queue userCreatedQueue) { + return bindingExchange(exchange, userCreatedQueue, userCreatedRoutingKey); + } + + @Bean + public Queue defaultQueue() { + return new Queue(defaultQueue,true); + } + + @Bean + public Queue userCreatedQueue() { + return new Queue(userCreatedQueue,true); + } + + public Binding bindingExchange(DirectExchange exchange, Queue queueMessage, String routingKey) { + return BindingBuilder.bind(queueMessage).to(exchange).with(routingKey); + } + +} + diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/config/RedisConfig.java b/sonic-bear/server/src/main/java/com/sonic/bear/config/RedisConfig.java new file mode 100644 index 0000000..358271f --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/config/RedisConfig.java @@ -0,0 +1,39 @@ +package com.sonic.bear.config; + +import com.sonic.common.utils.RedisLock; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * 有关Redis的配置 + * + * TODO: 如果不需要Redis, 可以删除该文件并且在{@link Config}类@Import的注解中移除对RedisConfig的引用 + */ +@Slf4j +public class RedisConfig { + + /** + * redisWrapper用于分布式锁RedisLock + * + * @param redisTemplate + * @return + */ + @Bean + RedisLock.RedisWrapper redisWrapper(RedisTemplate redisTemplate) { + return new RedisLock.RedisWrapper() { + @Override + public void delete(String key) { + redisTemplate.delete(key); + } + + @Override + public boolean lock(String key, String value, long expireMills) { + return redisTemplate.opsForValue().setIfAbsent(key, value, expireMills, TimeUnit.MILLISECONDS); + } + }; + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/config/RedissonConfig.java b/sonic-bear/server/src/main/java/com/sonic/bear/config/RedissonConfig.java new file mode 100644 index 0000000..58f0815 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/config/RedissonConfig.java @@ -0,0 +1,52 @@ +package com.sonic.bear.config; + +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + + /** + * 配置 RedissonClient,支持单机和集群模式 + */ + @Bean + public RedissonClient redissonClient(RedisProperties redisProperties) { + Config config = new Config(); + + // 设置 Netty 线程数,使用默认值 0(Redisson 默认使用 CPU 核心数的两倍) + config.setNettyThreads(0); + + // 判断是否配置了集群节点 + if (redisProperties.getCluster() != null && redisProperties.getCluster().getNodes() != null + && !redisProperties.getCluster().getNodes().isEmpty()) { + // 集群模式配置 + config.useClusterServers() + .setScanInterval(2000) // 集群状态扫描间隔,单位毫秒 + .addNodeAddress(redisProperties.getCluster().getNodes().stream() + .map(node -> "redis://" + node) + .toArray(String[]::new)) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null); + } else { + // 单机模式配置 + config.useSingleServer() + .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort()) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null) + .setDatabase(redisProperties.getDatabase()); + } + + // 如果启用了 SSL,设置 SSL 相关配置 + if (redisProperties.isSsl()) { + config.useSingleServer().setSslEnableEndpointIdentification(false); // 禁用主机名验证 + // 集群模式下,Redisson 会自动应用 SSL 配置 + } + + return Redisson.create(config); + } +} \ No newline at end of file diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/config/RestTemplateConfig.java b/sonic-bear/server/src/main/java/com/sonic/bear/config/RestTemplateConfig.java new file mode 100644 index 0000000..1520ebc --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/config/RestTemplateConfig.java @@ -0,0 +1,24 @@ +package com.sonic.bear.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(ClientHttpRequestFactory factory){ + return new RestTemplate(factory); + } + + @Bean + public ClientHttpRequestFactory simpleClientHttpRequestFactory(){ + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setReadTimeout(5000);//单位为ms + factory.setConnectTimeout(5000);//单位为ms + return factory; + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/config/ResultCode.java b/sonic-bear/server/src/main/java/com/sonic/bear/config/ResultCode.java new file mode 100644 index 0000000..c9c9e7d --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/config/ResultCode.java @@ -0,0 +1,43 @@ +package com.sonic.bear.config; + +import com.sonic.common.rpc.ApiResultCode; +import lombok.Getter; + +/** + * @author coder + */ +@Getter +public enum ResultCode implements ApiResultCode { + + /** + * TODO: 可以在此处扩展服务自身需要用到的错误码信息 + */ + BIZ_ERROR1("0000", "业务异常1"), + DEMO_CREATED_FAIL("0001", "新增Demo实体失败"); + + private final String errorCode; + private final String errorMsg; + + ResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/config/SwaggerConfig.java b/sonic-bear/server/src/main/java/com/sonic/bear/config/SwaggerConfig.java new file mode 100644 index 0000000..6761c26 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/config/SwaggerConfig.java @@ -0,0 +1,110 @@ +package com.sonic.bear.config; + + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import springfox.documentation.RequestHandler; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; + +/** + * swagger配置 + * @author code + */ +@Slf4j +@EnableSwagger2 +public class SwaggerConfig { + + private static final String SPLIT = ","; + + @Value("${swagger.enabled:false}") + private Boolean swaggerEnabled; + + @Value("${swagger.base.package}") + private String basePackageValue; + + private final String DEFAULT_BASE_PACKAGE = "com.sonic.bear.controller"; + + + public static Predicate basePackage(final String basePackage) { + return input -> declaringClass(input).transform(handlerPackage(basePackage)).or(true); + } + + private static Optional> declaringClass(RequestHandler input) { + return Optional.fromNullable(input.declaringClass()); + } + + private static Function, Boolean> handlerPackage(final String basePackage) { + return input -> { + // 循环判断匹配 + for (String strPackage : basePackage.split(SPLIT)) { + boolean isMatch = input.getPackage().getName().startsWith(strPackage); + if (isMatch) { + return true; + } + } + return false; + }; + } + + @Bean + public Docket createRestApi() { + basePackageValue = null == basePackageValue || ("").equals(basePackageValue) ? DEFAULT_BASE_PACKAGE : basePackageValue; + log.info("===> swagger base package : ", basePackageValue); + + //在配置好的配置类中增加此段代码即可 + ParameterBuilder ticketPar = new ParameterBuilder(); + List pars = new ArrayList(); + ticketPar.name("_tk_") + //name表示名称,description表示描述 + .description("token") + .modelRef(new ModelRef("string")) + .parameterType("header") + .required(false) + .defaultValue("") + .build();//required表示是否必填,defaultvalue表示默认值 + + //添加完此处一定要把下边的带***的也加上否则不生效 + pars.add(ticketPar.build()); + + + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .enable(swaggerEnabled) + .select() + .apis(basePackage(basePackageValue)) + .paths(PathSelectors.any()) + .build() + //************把消息头添加 + .globalOperationParameters(pars); + } + + /** + * TODO: 更改文案配置 + * @return + */ + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("sonic-bear") + .description("sonic-bear API") + .version("1.0") + .contact(new Contact("sonic-bear", "", "admin.sonic-bear.com")) + .build(); + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserLoginApi.java b/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserLoginApi.java new file mode 100644 index 0000000..5c4811d --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserLoginApi.java @@ -0,0 +1,66 @@ +package com.sonic.bear.controller.api; + +import com.sonic.bear.domain.input.KickOutInput; +import com.sonic.bear.domain.input.LogoutInput; +import com.sonic.bear.domain.input.ThirdUserLoginInput; +import com.sonic.bear.service.UserService; +import com.sonic.bear.service.UserSessionService; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.sdk.api.annotation.InternalRpc; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +/** + * API-账号登录管理 + * @author coder + */ +@InternalRpc(path = "/api/**") +@Validated +@RestController +public class UserLoginApi { + + @Autowired + private UserService userService; + @Autowired + private UserSessionService userSessionService; + + @ApiOperation(value = "三方账号登录", tags = {"API-账号登录"}) + @PostMapping("/api/auth/third-user-login") + public Result thirdUserLogin(@RequestBody @Valid ThirdUserLoginInput input) { + //执行账号验证操作 + Long userId = userService.thirdLoginOrRegister(input.getThirdType(), input.getOpenId(), input.getEmail(), input.getNickname()); + //执行登录操作 + UserSessionService.CreateSessionReq req = UserSessionService.CreateSessionReq.builder() + .userId(userId) + .clientCode(input.getClientCode()) + .loginType(input.getThirdType().name()) + .deviceId(input.getDeviceId()) + .ip(input.getIp()) + .userAgent(input.getUserAgent()) + .build(); + Session session = userSessionService.createSession(req); + return Result.success(session); + } + + @ApiOperation(value = "退出登录", tags = {"API-账号登录"}) + @PostMapping("/api/auth/logout") + public Result logout(@RequestBody @Valid LogoutInput input) { + userSessionService.logout(input); + return Result.success(); + } + + @ApiOperation(value = "踢下线", tags = {"API-账号登录"}) + @PostMapping("/api/auth/kick-out") + public Result kickOut(@RequestBody @Valid KickOutInput input) { + userSessionService.kickOut(input); + return Result.success(); + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserNicknamePoolApi.java b/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserNicknamePoolApi.java new file mode 100644 index 0000000..4fdaf97 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserNicknamePoolApi.java @@ -0,0 +1,49 @@ +package com.sonic.bear.controller.api; + +import com.sonic.bear.domain.input.IdInput; +import com.sonic.bear.domain.output.NicknameOutput; +import com.sonic.bear.lib.input.UserNicknameExistCheckInput; +import com.sonic.bear.lib.input.UserNicknameSyncPoolInput; +import com.sonic.bear.service.UserNicknamePoolService; +import com.sonic.common.rpc.Result; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +/** + * API-用户昵称 + * @author coder + */ +@Validated +@RestController +public class UserNicknamePoolApi { + + @Autowired + private UserNicknamePoolService userNicknamePoolService; + + @ApiOperation(value = "同步昵称数据到数据池", tags = {"API-用户昵称"}) + @PostMapping("/api/user-nickname/sync-pool") + public Result syncPool(@RequestBody @Valid UserNicknameSyncPoolInput input) { + userNicknamePoolService.syncPool(input.getUserId(), input.getUserType(), input.getNickname(), input.getOptType().name()); + return Result.success(); + } + + @ApiOperation(value = "批量查询昵称", tags = {"API-用户昵称"}) + @PostMapping("/api/user-nickname/batch-get") + public Result> batchGetNickname(@RequestBody @Valid IdInput input) { + return Result.success(userNicknamePoolService.batchGetNickname(input.getUserIdList())); + } + + @ApiOperation(value = "查询用户昵称是否存在", tags = {"API-用户昵称"}) + @PostMapping("/api/user-nickname/exist-check") + public Result existCheck(@RequestBody @Valid UserNicknameExistCheckInput input) { + return Result.success(userNicknamePoolService.existCheck(input.getExUserId(), input.getNickname())); + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserSearchApi.java b/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserSearchApi.java new file mode 100644 index 0000000..37dc3a0 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserSearchApi.java @@ -0,0 +1,43 @@ +package com.sonic.bear.controller.api; + +import com.sonic.bear.lib.input.UserIdInput; +import com.sonic.bear.lib.input.UserIdListInput; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.bear.lib.output.UserInfoListOutput; +import com.sonic.bear.service.UserSearchService; +import com.sonic.common.rpc.Result; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +/** + * API-用户查询 + * @author coder + */ +@Validated +@RestController +public class UserSearchApi { + + @Autowired + private UserSearchService userSearchService; + + @ApiOperation(value = "批量获取用户基础信息", tags = {"API-用户查询"}) + @PostMapping("/api/user/batch-get-info") + public Result> batchGetUserInfo(@RequestBody @Valid UserIdListInput input) { + return Result.success(userSearchService.batchGetUserInfo(input.getUserIdList())); + } + + @ApiOperation(value = "获取用户自己的基础信息", tags = {"API-用户查询"}) + @PostMapping("/api/user/base-user-info") + public Result baseUserInfo(@RequestBody @Valid UserIdInput input) { + return Result.success(userSearchService.baseUserInfo(input.getUserId())); + } + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserSessionApi.java b/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserSessionApi.java new file mode 100644 index 0000000..9b109ad --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserSessionApi.java @@ -0,0 +1,35 @@ +package com.sonic.bear.controller.api; + +import com.sonic.bear.domain.input.TouchSessionInput; +import com.sonic.bear.service.UserSessionService; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.log.IgnoreRequestLog; +import com.sonic.common.rpc.Result; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +/** + * API-Session获取 + * @author coder + */ +@Validated +@RestController +public class UserSessionApi { + + @Autowired + private UserSessionService userSessionService; + + @ApiOperation(value = "token验证", tags = {"API-Session获取"}) + @PostMapping("/api/auth/touch-session") + @IgnoreRequestLog(types = IgnoreRequestLog.IgnoreType.SILENT) + public Result touchSession(@RequestBody @Valid TouchSessionInput input) { + return Result.success(userSessionService.touchSession(input.getToken())); + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserSetApi.java b/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserSetApi.java new file mode 100644 index 0000000..2940448 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/controller/api/UserSetApi.java @@ -0,0 +1,51 @@ +package com.sonic.bear.controller.api; + +import com.sonic.bear.lib.input.CompleteUserInfoInput; +import com.sonic.bear.lib.input.EditUserInfoInput; +import com.sonic.bear.lib.input.UserIdInput; +import com.sonic.bear.service.UserSetService; +import com.sonic.common.rpc.Result; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +/** + * API-用户设置 + * @author coder + */ +@Validated +@RestController +public class UserSetApi { + + @Autowired + private UserSetService userSetService; + + @ApiOperation(value = "补全用户基础信息", tags = {"API-用户设置"}) + @PostMapping("/api/user/complete-user-info") + public Result completeUserInfo(@RequestBody @Valid CompleteUserInfoInput input) { + userSetService.completeUserInfo(input); + return Result.success(); + } + + @ApiOperation(value = "编辑用户基础信息", tags = {"API-用户设置"}) + @PostMapping("/api/user/edit-user-info") + public Result editUserInfo(@RequestBody @Valid EditUserInfoInput input) { + userSetService.editUserInfo(input); + return Result.success(); + } + + @ApiOperation(value = "删除账号", tags = {"API-用户设置"}) + @PostMapping("/api/user/del-account") + public Result delAccount(@RequestBody @Valid UserIdInput input) { + userSetService.delAccount(input.getUserId()); + return Result.success(); + } + + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/controller/mock/MockController.java b/sonic-bear/server/src/main/java/com/sonic/bear/controller/mock/MockController.java new file mode 100644 index 0000000..a2e1deb --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/controller/mock/MockController.java @@ -0,0 +1,74 @@ +package com.sonic.bear.controller.mock; + +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.RedisLock; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.concurrent.TimeUnit; + +/** + * @author: code + * @date: 2025/06/01 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@RestController +public class MockController { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private AppRuntime appRuntime; + @Autowired + private RedisTemplate redisTemplate; + + @IgnoreAuth + @ApiOperation(value = "redis连接测试", tags = "lion-冒烟") + @PostMapping("/mock/redis/test/set-get") + public Result testSetGet(Session session) { + String key = "aaa:111"; + stringRedisTemplate.opsForValue().set(key, "上海", 3, TimeUnit.MINUTES); + return Result.success(stringRedisTemplate.opsForValue().get(key)); + } + + @IgnoreAuth + @ApiOperation(value = "redis连接测试", tags = "lion-冒烟") + @PostMapping("/mock/redis/test/exp") + public Result exp(Session session) { + String key = "aaa:111"; + return Result.success(stringRedisTemplate.getExpire(key)); + } + + @IgnoreAuth + @ApiOperation(value = "redis加锁测试", tags = "lion-冒烟") + @PostMapping("/mock/redis/test/lock-v1") + public Result lockV1(Session session) { + String key = "aaa:333"; + //加锁,避免定时任务并发重复处理 + RedisLock redisLock = new RedisLock(key, redisWrapper); + redisLock.tryAcquireRun(10 * 60 * 1000, () -> { + System.out.println("=====> redis加锁测试 通过"); + try { + Thread.sleep(3000L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return true; + }); + return Result.success("V1 成功"); + } + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/controller/probe/ProbeController.java b/sonic-bear/server/src/main/java/com/sonic/bear/controller/probe/ProbeController.java new file mode 100644 index 0000000..dc20995 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/controller/probe/ProbeController.java @@ -0,0 +1,23 @@ +package com.sonic.bear.controller.probe; + +import com.sonic.common.auth.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 探测接口 + */ +@RestController +@Slf4j +public class ProbeController { + + @IgnoreAuth + @ApiOperation(value = "检测服务存活状态", tags = {"探针"}) + @PostMapping("/probe/check") + public void probeCheck() { + + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/dao/AppClientDao.java b/sonic-bear/server/src/main/java/com/sonic/bear/dao/AppClientDao.java new file mode 100644 index 0000000..25d6710 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/dao/AppClientDao.java @@ -0,0 +1,10 @@ +package com.sonic.bear.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.bear.domain.entity.AppClient; + +/** + * @author coder + */ +public interface AppClientDao extends BaseMapper { +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserAccessLogDao.java b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserAccessLogDao.java new file mode 100644 index 0000000..6087363 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserAccessLogDao.java @@ -0,0 +1,11 @@ +package com.sonic.bear.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.bear.domain.entity.UserAccessLog; + +/** + * @author coder + */ +public interface UserAccessLogDao extends BaseMapper { + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserCommonlyUsedLogDao.java b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserCommonlyUsedLogDao.java new file mode 100644 index 0000000..2608b3d --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserCommonlyUsedLogDao.java @@ -0,0 +1,11 @@ +package com.sonic.bear.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.bear.domain.entity.UserCommonlyUsedLog; + +/** + * @author coder + */ +public interface UserCommonlyUsedLogDao extends BaseMapper { + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserDao.java b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserDao.java new file mode 100644 index 0000000..9328615 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserDao.java @@ -0,0 +1,9 @@ +package com.sonic.bear.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.bear.domain.entity.User; + +public interface UserDao extends BaseMapper { + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserNicknamePoolDao.java b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserNicknamePoolDao.java new file mode 100644 index 0000000..65061ff --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserNicknamePoolDao.java @@ -0,0 +1,9 @@ +package com.sonic.bear.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.bear.domain.entity.UserNicknamePool; + +public interface UserNicknamePoolDao extends BaseMapper { + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserSessionDao.java b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserSessionDao.java new file mode 100644 index 0000000..890d8a4 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserSessionDao.java @@ -0,0 +1,19 @@ +package com.sonic.bear.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.bear.domain.entity.UserSession; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +/** + * @author coder + */ +public interface UserSessionDao extends BaseMapper { + + @Update("update user_session set status = 'EXPIRED' where user_id = #{userId} AND client_code= #{clientCode} AND status= 'ENABLED' ") + int expiredSession(@Param("userId") Long userId, @Param("clientCode") String clientCode); + + @Update("update user_session set status = 'EXPIRED' where user_id = #{userId} AND status= 'ENABLED'") + int expiredSessionV2(@Param("userId") Long userId); + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserThirdDao.java b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserThirdDao.java new file mode 100644 index 0000000..a713a03 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/dao/UserThirdDao.java @@ -0,0 +1,9 @@ +package com.sonic.bear.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.bear.domain.entity.UserThird; + +public interface UserThirdDao extends BaseMapper { + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/AppClient.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/AppClient.java new file mode 100644 index 0000000..059e361 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/AppClient.java @@ -0,0 +1,51 @@ +package com.sonic.bear.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.bear.domain.typehandler.AccountTypeSetTypeHandler; +import com.sonic.bear.enums.AccountType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Set; + +/** + * @author coder + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "app_client", autoResultMap = true) +public class AppClient { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + /** 编码 */ + private String code; + /** 描述 */ + @TableField("`desc`") + private String desc; + /** 允许登陆的账号类型 */ + @TableField(typeHandler = AccountTypeSetTypeHandler.class) + private Set allowAccountTypes; + /** session过期时间,单位为分钟 */ + private Integer sessionAlive; + /** session存活时间, session 在未过期的情况,最大可以续存时间,单位为分钟 */ + private Integer sessionExpired; + /** 创建时间 */ + private LocalDateTime createTime; + /** 更新时间 */ + private LocalDateTime editTime; + + public boolean allowUser(AccountType accountType) { + if (this.getAllowAccountTypes().isEmpty()) { + return false; + } + return this.getAllowAccountTypes().stream().anyMatch(type -> type == accountType); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/User.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/User.java new file mode 100644 index 0000000..1afb0fb --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/User.java @@ -0,0 +1,106 @@ +package com.sonic.bear.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户实体类 + * + * @author coder + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("user") +public class User { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 用户ID(至少15位) + */ + @TableField("user_id") + private Long userId; + + /** + * ID编号 + */ + @TableField("id_card") + private String idCard; + + /** + * 头像 + */ + @TableField("head_image") + private String headImage; + + /** + * 昵称 + */ + @TableField("nickname") + private String nickname; + + /** + * 生日 + */ + @TableField("birthday") + private LocalDateTime birthday; + + /** + * 性别(0 男、1 女、2 未知) + */ + @TableField("sex") + private Integer sex; + + /** + * 账户类型(1 客户、2 后台) + */ + @TableField("account_type") + private Integer accountType; + + /** + * 账户状态(0 正常、1 禁用、2 锁定) + */ + @TableField("account_status") + private Integer accountStatus; + + /** + * 最后访问时间 + */ + @TableField("last_access_time") + private LocalDateTime lastAccessTime; + + /** + * 是否删除(0 否、1 是) + */ + @TableField("is_delete") + private Boolean isDelete; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 编辑人 + */ + @TableField("edit_id") + private Long editId; + + /** + * 编辑时间 + */ + @TableField(value = "edit_time", update = "NOW()") + private LocalDateTime editTime; +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserAccessLog.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserAccessLog.java new file mode 100644 index 0000000..566ee34 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserAccessLog.java @@ -0,0 +1,37 @@ +package com.sonic.bear.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * @author coder + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@TableName(value = "user_access_log", autoResultMap = true) +public class UserAccessLog { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** 账号 Id */ + private Long userId; + + /**客户端类型*/ + private String clientCode; + + /** 访问时间 */ + private LocalDateTime accessTime; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserCommonlyUsedLog.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserCommonlyUsedLog.java new file mode 100644 index 0000000..94d79e0 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserCommonlyUsedLog.java @@ -0,0 +1,61 @@ +package com.sonic.bear.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * @Author code + * @Description 用户常用日志 + * @Date 2024/1/12 11:57 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@TableName(value = "user_commonly_used_log", autoResultMap = true) +public class UserCommonlyUsedLog { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 数据类型(1 IP地址、2 设备好) + */ + private Integer dataType; + + /** + * 数据值 + */ + private String dataValue; + + /** + * 使用次数 + */ + private Integer useCount; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime editTime; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserInfoBo.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserInfoBo.java new file mode 100644 index 0000000..9112bf9 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserInfoBo.java @@ -0,0 +1,35 @@ +package com.sonic.bear.domain.entity; + +import com.sonic.bear.enums.AccountType; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class UserInfoBo { + + /** + * 用户ID + */ + private Long userId; + + /** + * 账号类型 + */ + private Integer accountType; + + /** + * 账号状态 + */ + private Integer accountStatus; + + /** + * 账号注册时间 + */ + private LocalDateTime createTime; + + public AccountType getAccountTypeEnum() { + return AccountType.from(this.accountType); + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserNicknamePool.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserNicknamePool.java new file mode 100644 index 0000000..3ef88c6 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserNicknamePool.java @@ -0,0 +1,56 @@ +package com.sonic.bear.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户昵称数据池实体类 + * + * @author coder + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("user_nickname_pool") +public class UserNicknamePool { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 用户ID(普通用户ID、AI用户ID) + */ + @TableField("user_id") + private Long userId; + + /** + * 用户类型(1 USER、2 AI) + */ + @TableField("user_type") + private Integer userType; + + /** + * 昵称 + */ + @TableField("nickname") + private String nickname; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserSession.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserSession.java new file mode 100644 index 0000000..11cdd0b --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserSession.java @@ -0,0 +1,116 @@ +package com.sonic.bear.domain.entity; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.annotation.JSONField; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler; +import com.google.common.base.Strings; +import com.sonic.bear.enums.AccountType; +import com.sonic.common.auth.domains.SessionStatusEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * @author coder + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@TableName(value = "user_session", autoResultMap = true) +public class UserSession { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + /** 登陆记号 */ + private String token; + /** 账号 Id */ + private Long userId; + /** 登陆的账号类型 */ + @TableField(typeHandler = FastjsonTypeHandler.class) + private AccountType accountType; + /** 客户端编码 */ + private String clientCode; + /*** + * 登录类型(账号密码、三方、手机号验证码) + */ + private String loginType; + /** session 状态,默认为有效 */ + private SessionStatusEnum status; + + /** + * 客户端设备基础信息 + */ + private String userAgent; + /** 扩展数据,json字符串 */ + private String extra; + /** + * 登录的设备ID + */ + private String deviceId; + /** + * 登录的IP地址 + */ + private String ip; + /** session 创建时间 */ + private LocalDateTime createTime; + /** session 最后一次访问时间 */ + private LocalDateTime lastAccessTime; + /** session 过期/结束时间 */ + private LocalDateTime expireTime; + + @JSONField(serialize = false) + public boolean isEnabled() { + return status == SessionStatusEnum.ENABLED; + } + + @JSONField(serialize = false) + public boolean isDisabled() { + return status != SessionStatusEnum.ENABLED; + } + + @JSONField(serialize = false) + public boolean isExpired() { + return expireTime != null && LocalDateTime.now().isAfter(expireTime); + } + + /** + * 是否超过最大存活期内 + * @param sessionAlive 最大存活时间,单位分钟,由 client 定义 + * @return 是否在最大存活时间内 + */ + @JSONField(serialize = false) + public boolean isOverMaxAlive(Integer sessionAlive) { + LocalDateTime maxAliveTime = createTime.plusMinutes(sessionAlive); + return LocalDateTime.now().isAfter(maxAliveTime); + } + + /** 将extra转为对象 */ + public Extra normalizedExtra() { + return JSON.parseObject(extra, Extra.class); + } + + public static Extra parseExtraObject(String text) { + if (Strings.isNullOrEmpty(text)) { + return new Extra(); + } + return JSON.parseObject(text, Extra.class); + } + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class Extra { + private String email; + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserThird.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserThird.java new file mode 100644 index 0000000..2342bae --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/entity/UserThird.java @@ -0,0 +1,82 @@ +package com.sonic.bear.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户三方信息实体类 + * + * @author coder + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("user_third") +public class UserThird { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.NONE) + private Long id; + + /** + * 用户ID + */ + @TableField("user_id") + private Long userId; + + /** + * 三方类型(DISCORD、GOOGLE、APPLE)详见:ThirdTypeEnum + */ + @TableField("third_type") + private String thirdType; + + /** + * 三方ID + */ + @TableField("open_id") + private String openId; + + /** + * 昵称 + */ + @TableField("nickname") + private String nickname; + + /** + * 邮箱 + */ + @TableField("email") + private String email; + + /** + * 是否删除(0 否、1 是) + */ + @TableField("is_delete") + private Boolean isDelete; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 编辑人 + */ + @TableField("edit_id") + private Long editId; + + /** + * 编辑时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/IdInput.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/IdInput.java new file mode 100644 index 0000000..cafa8fb --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/IdInput.java @@ -0,0 +1,24 @@ +package com.sonic.bear.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @Author code + * @Description TODO + * @Date 2022/11/14 20:19 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IdInput { + + List userIdList; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/InitExistCacheInput.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/InitExistCacheInput.java new file mode 100644 index 0000000..055433f --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/InitExistCacheInput.java @@ -0,0 +1,24 @@ +package com.sonic.bear.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @Author code + * @Description TODO + * @Date 2022/11/14 20:19 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InitExistCacheInput { + + List idList; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/KickOutInput.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/KickOutInput.java new file mode 100644 index 0000000..85b26e2 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/KickOutInput.java @@ -0,0 +1,14 @@ +package com.sonic.bear.domain.input; + +import io.swagger.annotations.ApiParam; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Data +public class KickOutInput { + + @ApiParam("用户ID") + @NotNull + private Long userId; +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/LogoutInput.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/LogoutInput.java new file mode 100644 index 0000000..3e05e33 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/LogoutInput.java @@ -0,0 +1,15 @@ +package com.sonic.bear.domain.input; + +import io.swagger.annotations.ApiParam; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Data +public class LogoutInput { + + @ApiParam("登录授权码") + @NotEmpty + private String token; +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/ThirdUserLoginInput.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/ThirdUserLoginInput.java new file mode 100644 index 0000000..f4eb0a7 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/ThirdUserLoginInput.java @@ -0,0 +1,40 @@ +package com.sonic.bear.domain.input; + +import com.sonic.bear.enums.ThirdTypeEnum; +import io.swagger.annotations.ApiParam; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +public class ThirdUserLoginInput { + + @ApiParam("三方ID") + @NotBlank + private String openId; + + @ApiParam("三方邮箱") + private String email; + + @ApiParam("三方昵称") + private String nickname; + + @ApiParam("三方类型") + @NotNull + private ThirdTypeEnum thirdType; + + @ApiParam("客户端标识") + @NotBlank + private String clientCode; + + @ApiParam("设备ID") + private String deviceId; + + @ApiParam("IP地址") + private String ip; + + @ApiParam("UA") + private String userAgent; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/TouchSessionInput.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/TouchSessionInput.java new file mode 100644 index 0000000..c26eabe --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/TouchSessionInput.java @@ -0,0 +1,13 @@ +package com.sonic.bear.domain.input; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class TouchSessionInput { + + @NotBlank(message = "token not null") + private String token; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/UserCommonlyUsedLogInput.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/UserCommonlyUsedLogInput.java new file mode 100644 index 0000000..708ec4c --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/input/UserCommonlyUsedLogInput.java @@ -0,0 +1,20 @@ +package com.sonic.bear.domain.input; + +import lombok.Data; + +import java.util.List; + +/** + * @Author code + * @Description TODO + * @Date 2024/1/12 19:25 + * @Version 1.0 + */ +@Data +public class UserCommonlyUsedLogInput { + + List userIdList; + + List dataValueList; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/output/NicknameOutput.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/output/NicknameOutput.java new file mode 100644 index 0000000..0ff16af --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/output/NicknameOutput.java @@ -0,0 +1,30 @@ +package com.sonic.bear.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author code + * @Description 用户昵称 + * @Date 2024/1/12 11:57 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NicknameOutput { + + /** + * 用户ID + */ + private Long userId; + + /** + * 昵称 + */ + private String nickname; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/output/UserCommonlyUsedLogOutput.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/output/UserCommonlyUsedLogOutput.java new file mode 100644 index 0000000..347adff --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/output/UserCommonlyUsedLogOutput.java @@ -0,0 +1,35 @@ +package com.sonic.bear.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author code + * @Description 用户常用日志 + * @Date 2024/1/12 11:57 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserCommonlyUsedLogOutput { + + /** + * 用户ID + */ + private Long userId; + + /** + * 数据类型(1 IP地址、2 设备好) + */ + private Integer dataType; + + /** + * 数据值 + */ + private String dataValue; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/domain/typehandler/AccountTypeSetTypeHandler.java b/sonic-bear/server/src/main/java/com/sonic/bear/domain/typehandler/AccountTypeSetTypeHandler.java new file mode 100644 index 0000000..d57ee37 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/domain/typehandler/AccountTypeSetTypeHandler.java @@ -0,0 +1,13 @@ +package com.sonic.bear.domain.typehandler; + + +import com.sonic.daosupport.mysql.typehandler.BaseSetTypeHandler; +import com.sonic.bear.enums.AccountType; + +/** + * fastjsonTypeHandler 解析 Set 不会解析泛型 + * @author coder + * @date 2020-04-15 + */ +public class AccountTypeSetTypeHandler extends BaseSetTypeHandler { +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/enums/AccountType.java b/sonic-bear/server/src/main/java/com/sonic/bear/enums/AccountType.java new file mode 100644 index 0000000..3e3a80b --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/enums/AccountType.java @@ -0,0 +1,36 @@ +package com.sonic.bear.enums; + +import lombok.Getter; + +/** + * 账号类型 + * @author coder + */ +@Getter +public enum AccountType { + /** + * 客户用户 + */ + CUSTOMER(1), + /** + * 后台账号 + */ + SYSTEM(2), + ; + + private final Integer code; + + AccountType(Integer code) { + this.code = code; + } + + public static AccountType from(Integer code) { + for (AccountType status : AccountType.values()) { + if (code.equals(status.code)) { + return status; + } + } + + return null; + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/enums/AppClientType.java b/sonic-bear/server/src/main/java/com/sonic/bear/enums/AppClientType.java new file mode 100644 index 0000000..720a1da --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/enums/AppClientType.java @@ -0,0 +1,31 @@ +package com.sonic.bear.enums; + +import lombok.Getter; + +/** + * 应用端编码,数据来自 app_client.code + * @author coder + */ +@Getter +public enum AppClientType { + + /** + * web 端 + */ + WEB, + /** + * 管理后台 + */ + ADMIN, + /** + * ios 端 + */ + IOS, + + /** + * android 端 + */ + ANDROID, + ; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/enums/FallBackBizTypeEnum.java b/sonic-bear/server/src/main/java/com/sonic/bear/enums/FallBackBizTypeEnum.java new file mode 100644 index 0000000..0eb5caf --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/enums/FallBackBizTypeEnum.java @@ -0,0 +1,34 @@ +package com.sonic.bear.enums; + +import lombok.Getter; + +/** + * 降级的业务类型 + * @Author code + * @Date 2022/1/12 + * @Version 1.0 + */ +@Getter +public enum FallBackBizTypeEnum { + + /** SSO服务、3分钟内3次 */ + SSO(2, 360), + + ; + + /** + * 超时次数 + */ + private Integer timeOutCount; + + /** + * 窗口时间(单位:秒) + */ + private Integer timeSeconds; + + + FallBackBizTypeEnum(Integer timeOutCount, Integer timeSeconds) { + this.timeOutCount = timeOutCount; + this.timeSeconds = timeSeconds; + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/enums/ThirdTypeEnum.java b/sonic-bear/server/src/main/java/com/sonic/bear/enums/ThirdTypeEnum.java new file mode 100644 index 0000000..44752e9 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/enums/ThirdTypeEnum.java @@ -0,0 +1,13 @@ +package com.sonic.bear.enums; + +/** + * 三方类型枚举 + */ +public enum ThirdTypeEnum { + + DISCORD, + GOOGLE, + APPLE, + ; + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/enums/ToastResultCode.java b/sonic-bear/server/src/main/java/com/sonic/bear/enums/ToastResultCode.java new file mode 100644 index 0000000..99cea2c --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/enums/ToastResultCode.java @@ -0,0 +1,92 @@ +package com.sonic.bear.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum ToastResultCode implements ApiResultCode { + + /** 可以在此处扩展服务自身需要用到的错误码信息 */ + BIZ_ERROR1("-1", "biz error"), + NO_LOGIN("0001", "User is not logged in"), + USER_NOT_EXIST("0002", "User does not exist"), + APP_CLIENT_NOT_EXIST("0003", "Login client does not exist"), + APP_CLIENT_NOT_ALLOW("0004", "Not authorized to log in to the client"), + SESSION_EXPIRED("0005", "Session is expired"), + SESSION_INVALID("0006", "Session is invalid"), + PASSWORD_INVALID("0007", "Invalid account or password"), + /** 第三方登陆授权失败 */ + AUTH_FAIL("0008","auth.fail"), + USER_FROZEN("0009", "User is frozen"), + + ; + + + private final String errorCode; + private final String errorMsg; + + ToastResultCode(String errorMsg) { + this.errorCode = ""; + this.errorMsg = errorMsg; + } + + ToastResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect, String msg) { + if (expect) { + if(StringUtils.isNotBlank(msg)) { + throw new BizException(this.getErrorCode(), msg); + + } else { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/event/inner/EventType.java b/sonic-bear/server/src/main/java/com/sonic/bear/event/inner/EventType.java new file mode 100644 index 0000000..807f6df --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/event/inner/EventType.java @@ -0,0 +1,30 @@ +package com.sonic.bear.event.inner; + +import com.sonic.bear.config.EventConfig; +import com.sonic.common.event.Event; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义当前系统的消息 + * @author coder + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** 事件定义 */ + DEMO_CREATED(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "demo_created", "demo 创建"), + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/event/inner/handler/DemoCreatedThenHandler.java b/sonic-bear/server/src/main/java/com/sonic/bear/event/inner/handler/DemoCreatedThenHandler.java new file mode 100644 index 0000000..d1a6943 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/event/inner/handler/DemoCreatedThenHandler.java @@ -0,0 +1,33 @@ +package com.sonic.bear.event.inner.handler; + +import com.sonic.bear.event.inner.EventType; +import com.sonic.bear.event.inner.payload.DemoCreatedPayload; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author coder + */ +@Slf4j +@Component +public class DemoCreatedThenHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + + @Override + public void onEvent(Event event) { + DemoCreatedPayload payload = event.normalizedData(DemoCreatedPayload.class); + // TODO: + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.DEMO_CREATED.getEventCode(), this); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/event/inner/payload/DemoCreatedPayload.java b/sonic-bear/server/src/main/java/com/sonic/bear/event/inner/payload/DemoCreatedPayload.java new file mode 100644 index 0000000..d1448f9 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/event/inner/payload/DemoCreatedPayload.java @@ -0,0 +1,18 @@ +package com.sonic.bear.event.inner.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author coder + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DemoCreatedPayload { + private Long demoId; +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/event/outer/EventType.java b/sonic-bear/server/src/main/java/com/sonic/bear/event/outer/EventType.java new file mode 100644 index 0000000..0f85ee0 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/event/outer/EventType.java @@ -0,0 +1,29 @@ +package com.sonic.bear.event.outer; + +import com.sonic.common.event.Event; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义不属于当前系统的消息 + * @author coder + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** 事件定义 */ + USER_CREATED(Event.BuildInScene.BS.getCode(), "bs_user", "user_created", "用户创建"), + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/event/outer/handler/UserCreatedThenHandler.java b/sonic-bear/server/src/main/java/com/sonic/bear/event/outer/handler/UserCreatedThenHandler.java new file mode 100644 index 0000000..b100dfb --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/event/outer/handler/UserCreatedThenHandler.java @@ -0,0 +1,78 @@ +package com.sonic.bear.event.outer.handler; + +import com.sonic.bear.event.outer.EventType; +import com.sonic.bear.event.outer.payload.UserCratedPayload; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.bear.service.CommonMessageService; +import com.sonic.bear.service.UserSearchService; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.input.PlatformGiftInput; +import com.sonic.pigeon.lib.client.ImUserClient; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import com.sonic.pigeon.lib.input.CreateImUserInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * @author coder + */ +@Slf4j +@Component +public class UserCreatedThenHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private ImUserClient imUserClient; + @Autowired + private PayClient payClient; + @Autowired + private CommonMessageService commonMessageService; + @Autowired + private UserSearchService userSearchService; + + @Override + public void onEvent(Event event) { + UserCratedPayload payload = event.normalizedData(UserCratedPayload.class); + log.info("UserCreatedThenHandler payload:{}", payload); + if (payload.getUserId() == null) { + return; + } + Long userId = payload.getUserId(); + //获取用户信息 + BaseUserInfoOutput baseUserInfo = userSearchService.baseUserInfo(userId); + //设置创建IM账号的入参对象 + CreateImUserInput createImUserInput = new CreateImUserInput(); + createImUserInput.setUserId(baseUserInfo.getUserId()); + createImUserInput.setNickname(baseUserInfo.getNickname()); + createImUserInput.setHeadImage(baseUserInfo.getHeadImage()); + createImUserInput.setImUserType(ImUserTypeEnum.u); + //RPC 调用IM创建账号 + imUserClient.createImUser(createImUserInput); + //RPC 初始化账号钱包 + payClient.createAccountBuff(userId); + //用户注册发送欢迎系统通知 + commonMessageService.userRegisterSendMessage(userId); + //赠送5个BUFF + PlatformGiftInput platformGiftInput = new PlatformGiftInput(); + platformGiftInput.setPlatform("1"); + platformGiftInput.setUid(userId); + platformGiftInput.setName(PlatformGiftInput.Type.NEW_USER_GIFT.getBizType().getDesc()); + platformGiftInput.setCreateTime(LocalDateTime.now()); + platformGiftInput.setAmount(500L); + platformGiftInput.setType(PlatformGiftInput.Type.NEW_USER_GIFT); + payClient.platformGift(platformGiftInput); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.USER_CREATED.getEventCode(), this); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/event/outer/payload/UserCratedPayload.java b/sonic-bear/server/src/main/java/com/sonic/bear/event/outer/payload/UserCratedPayload.java new file mode 100644 index 0000000..1796f35 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/event/outer/payload/UserCratedPayload.java @@ -0,0 +1,18 @@ +package com.sonic.bear.event.outer.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author coder + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserCratedPayload { + private Long userId; +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/AppClientService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/AppClientService.java new file mode 100644 index 0000000..db53dc6 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/AppClientService.java @@ -0,0 +1,11 @@ +package com.sonic.bear.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.bear.domain.entity.AppClient; + +import java.util.Optional; + +public interface AppClientService extends IService { + + Optional getByCode(String code); +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/CommonMessageService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/CommonMessageService.java new file mode 100644 index 0000000..079a360 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/CommonMessageService.java @@ -0,0 +1,17 @@ +package com.sonic.bear.service; + + +/** + * 公共发送系统通知 + */ +public interface CommonMessageService { + + /** + * 用户注册发送欢迎系统通知 + * + * @param + */ + void userRegisterSendMessage(Long userId); + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/CommonSendMqService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/CommonSendMqService.java new file mode 100644 index 0000000..2ddc75b --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/CommonSendMqService.java @@ -0,0 +1,17 @@ +package com.sonic.bear.service; + + +/** + * 发送消息到im的mq + */ +public interface CommonSendMqService { + + /** + * 用户创建成功后发送消息 + * + * @param userId + */ + void userCreatedSendMq(Long userId); + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/SessionCacheService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/SessionCacheService.java new file mode 100644 index 0000000..624b97e --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/SessionCacheService.java @@ -0,0 +1,16 @@ +package com.sonic.bear.service; + +import com.sonic.common.auth.domains.Session; + +import java.util.Optional; + +public interface SessionCacheService { + + Optional get(String token); + + void cache(String token, Session session); + + void localCache(String token, Session session); + + void invalidate(String token); +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/SessionExistVerifyCacheService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/SessionExistVerifyCacheService.java new file mode 100644 index 0000000..3d24614 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/SessionExistVerifyCacheService.java @@ -0,0 +1,47 @@ +package com.sonic.bear.service; + +import java.util.Optional; + +/** + * @Author code + * @Description 用户Token存在性验证缓存,防止穿透 + * @Date 2022/11/14 19:02 + * @Version 1.0 + */ +public interface SessionExistVerifyCacheService { + + /** + * 获取缓存数据 + * @param token + * @return + */ + Optional get(String token); + + /** + * 校验当前访问的token是否存在 + * @param token + * @return + */ + boolean exist(String token); + + /** + * 设置数据到内存缓存和redis缓存 + * @param token + * @param expTime 过期时间(单位:秒 s) + */ + void cache(String token, Long expTime); + + /** + * 初始化数据 + * @param token + * @param expTime + */ + void initCache(String token, Long expTime); + + /** + * 清理掉缓存的存在的token数据 + * @param token + */ + void clearCache(String token); + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/SessionKickOutCacheService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/SessionKickOutCacheService.java new file mode 100644 index 0000000..04074c2 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/SessionKickOutCacheService.java @@ -0,0 +1,12 @@ +package com.sonic.bear.service; + +import java.util.Optional; + +public interface SessionKickOutCacheService { + + Optional get(String token); + + void cache(String token); + + void invalidate(String token); +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/UserAccessLogService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserAccessLogService.java new file mode 100644 index 0000000..28abf6b --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserAccessLogService.java @@ -0,0 +1,18 @@ +package com.sonic.bear.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.bear.domain.entity.UserAccessLog; + +/** + * @author coder + */ +public interface UserAccessLogService extends IService { + + /** + * 添加访问日志记录 + * @param userId + * @param clientCode + */ + void addLog(Long userId, String clientCode); + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/UserCommonlyUsedLogService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserCommonlyUsedLogService.java new file mode 100644 index 0000000..1d15658 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserCommonlyUsedLogService.java @@ -0,0 +1,30 @@ +package com.sonic.bear.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.bear.domain.entity.UserCommonlyUsedLog; + +import java.util.List; + +/** + * @author coder + */ +public interface UserCommonlyUsedLogService extends IService { + + + /** + * 添加数据 + * @param userId + * @param dataType + * @param dataValue + */ + void addData(Long userId, Integer dataType, String dataValue); + + /** + * 查询数据 + * @param userIdList + * @param dataValueList + * @return + */ + List queryData(List userIdList, List dataValueList); + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/UserNicknamePoolService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserNicknamePoolService.java new file mode 100644 index 0000000..ade1c5a --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserNicknamePoolService.java @@ -0,0 +1,37 @@ +package com.sonic.bear.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.bear.domain.entity.UserNicknamePool; +import com.sonic.bear.domain.output.NicknameOutput; + +import java.util.List; + +/** + * @author coder + */ +public interface UserNicknamePoolService extends IService { + + /** + * 同步昵称池 + * @param userId + * @param userType + * @param nickname + * @param optType + */ + void syncPool(Long userId, Integer userType, String nickname, String optType); + + /** + * 批量获取昵称池 + * @param userIdList + */ + List batchGetNickname(List userIdList); + + /** + * 校验昵称是否已经存在 + * @param userId + * @param nickname + * @return + */ + boolean existCheck(Long userId, String nickname); + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/UserSearchService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserSearchService.java new file mode 100644 index 0000000..d14fda9 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserSearchService.java @@ -0,0 +1,27 @@ +package com.sonic.bear.service; + +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.bear.lib.output.UserInfoListOutput; + +import java.util.List; + +/** + * @author coder + */ +public interface UserSearchService { + + /** + * 批量获取用户基础信息 + * @param userIdList + * @return + */ + List batchGetUserInfo(List userIdList); + + /** + * 获取用户自己的基础信息 + * @param userId + * @return + */ + BaseUserInfoOutput baseUserInfo(Long userId); + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/UserService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserService.java new file mode 100644 index 0000000..3eb2d67 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserService.java @@ -0,0 +1,31 @@ +package com.sonic.bear.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.bear.domain.entity.User; +import com.sonic.bear.domain.entity.UserInfoBo; +import com.sonic.bear.enums.ThirdTypeEnum; + +/** + * @author coder + */ +public interface UserService extends IService { + + + /** + * 三方登录或注册 + * @param thirdType + * @param openId + * @param email + * @param nickname + * @return + */ + Long thirdLoginOrRegister(ThirdTypeEnum thirdType, String openId, String email, String nickname); + + /** + * 查询用户信息 + * @param userId + * @return + */ + UserInfoBo queryUserInfo(Long userId); + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/UserSessionService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserSessionService.java new file mode 100644 index 0000000..50a41b1 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserSessionService.java @@ -0,0 +1,80 @@ +package com.sonic.bear.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.bear.domain.input.KickOutInput; +import com.sonic.bear.domain.input.LogoutInput; +import com.sonic.common.auth.domains.Session; +import com.sonic.bear.domain.entity.UserSession; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Optional; + +/** + * @author coder + */ +public interface UserSessionService extends IService { + + /** + * 创建一个 Session + * @param param + * @return + */ + Session createSession(CreateSessionReq param); + + /** + * 验证token是否有效 + * @param token session 令牌 + * @return 有效的 session,无效则抛出异常 + */ + Session touchSession(String token); + + /** + * 退出登录。 + * 1.更新session状态 + * 2.更新loginToken状态 + * 3.使session缓存过期 + * @param input + */ + void logout(LogoutInput input); + + /** + * 根据 token 查询 UserSession + * @param token + * @return + */ + Optional get(String token); + + /** + * 踢下线 + * @param input + */ + void kickOut(KickOutInput input); + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + class CreateSessionReq { + @NotNull(message = "{YOU_HAVE_TO_ENTER_YOUR_ACCOUNT}") + private Long userId; + @NotBlank(message = "{YOU_HAVE_TO_SPECIFY_THE_LOGIN_CLIENT}") + private String clientCode; + private String deviceId; + private String ip; + /** + * 登录类型(三方账号、账号密码、手机号验证码 等) + */ + private String loginType; + /** + * 客户端设备基础信息 + */ + private String userAgent; + } + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/UserSetService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserSetService.java new file mode 100644 index 0000000..fd16492 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserSetService.java @@ -0,0 +1,28 @@ +package com.sonic.bear.service; + +import com.sonic.bear.lib.input.CompleteUserInfoInput; +import com.sonic.bear.lib.input.EditUserInfoInput; + +/** + * @author coder + */ +public interface UserSetService { + + /** + * 补全用户信息 + * @param input + */ + void completeUserInfo(CompleteUserInfoInput input); + + /** + * 编辑用户信息 + * @param input + */ + void editUserInfo(EditUserInfoInput input); + + /** + * 删除账号 + * @param userId + */ + void delAccount(Long userId); +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/UserThirdService.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserThirdService.java new file mode 100644 index 0000000..b466453 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/UserThirdService.java @@ -0,0 +1,19 @@ +package com.sonic.bear.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.bear.domain.entity.UserThird; +import com.sonic.bear.enums.ThirdTypeEnum; + +/** + * @author coder + */ +public interface UserThirdService extends IService { + + /** + * 根据三方类型获取基础数据 + * @param thirdType + * @param openId + * @return + */ + UserThird queryByOpenId(ThirdTypeEnum thirdType, String openId); +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/AppClientServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/AppClientServiceImpl.java new file mode 100644 index 0000000..fdfe4ca --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/AppClientServiceImpl.java @@ -0,0 +1,52 @@ +package com.sonic.bear.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.base.Strings; +import com.sonic.bear.dao.AppClientDao; +import com.sonic.bear.domain.entity.AppClient; +import com.sonic.bear.service.AppClientService; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author coder + */ +@Service +public class AppClientServiceImpl extends ServiceImpl implements AppClientService, InitializingBean { + + LoadingCache> appClientCache; + + @Override + public Optional getByCode(String code) { + if (Strings.isNullOrEmpty(code)) { + return Optional.empty(); + } + + Map codeAppClientMap = appClientCache.get(""); + if (codeAppClientMap == null) { + return Optional.empty(); + } + + return Optional.ofNullable(codeAppClientMap.get(code)); + } + + @Override + public void afterPropertiesSet() { + appClientCache = Caffeine.newBuilder() + .maximumSize(20) + .initialCapacity(5) + .recordStats() + //缓存24小时 + .expireAfterWrite(Duration.ofSeconds(24 * 60 * 60)) + .build(key -> baseMapper.selectList(null).stream() + .collect(Collectors.toMap(AppClient::getCode, Function.identity()))); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/CommonMessageServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/CommonMessageServiceImpl.java new file mode 100644 index 0000000..80c5e3d --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/CommonMessageServiceImpl.java @@ -0,0 +1,29 @@ +package com.sonic.bear.service.impl; + + +import com.sonic.bear.service.CommonMessageService; +import com.sonic.pigeon.lib.client.MessageClient; +import com.sonic.pigeon.lib.enums.StationMessageTypeEnum; +import com.sonic.pigeon.lib.input.SendMessageInput; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 公共发送系统通知 + */ +@Service +public class CommonMessageServiceImpl implements CommonMessageService { + + @Autowired + private MessageClient messageClient; + + + @Override + public void userRegisterSendMessage(Long userId) { + String title = StationMessageTypeEnum.REGISTER_WELCOME.getTitle(); + String content = StationMessageTypeEnum.REGISTER_WELCOME.getContent(); + SendMessageInput sendMessageInput = new SendMessageInput(-1L, userId, StationMessageTypeEnum.REGISTER_WELCOME.getIndex(), title, content); + messageClient.sendMessage(sendMessageInput); + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/CommonSendMqServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/CommonSendMqServiceImpl.java new file mode 100644 index 0000000..3b437b0 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/CommonSendMqServiceImpl.java @@ -0,0 +1,41 @@ +package com.sonic.bear.service.impl; + + +import com.sonic.bear.event.outer.payload.UserCratedPayload; +import com.sonic.bear.service.CommonSendMqService; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventProducer; +import com.sonic.common.event.RabbitmqEventProducer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +import static com.sonic.bear.event.outer.EventType.USER_CREATED; + +/** + * 发送消息到im的mq + */ +@Service +@Slf4j +public class CommonSendMqServiceImpl implements CommonSendMqService { + + @Autowired + private EventProducer eventProducer; + + @Autowired + @Qualifier("userCreatedMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta userCreatedMeta; + + @Override + public void userCreatedSendMq(Long userId) { + UserCratedPayload payload = UserCratedPayload.builder() + .userId(userId) + .build(); + eventProducer.send(Event.builder() + .eventScene(USER_CREATED.getEventCode().getScene()) + .eventModule(USER_CREATED.getEventCode().getModule()) + .eventName(USER_CREATED.getEventCode().getName()) + .data(payload).build(), userCreatedMeta); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/SessionCacheServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/SessionCacheServiceImpl.java new file mode 100644 index 0000000..f6e0190 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/SessionCacheServiceImpl.java @@ -0,0 +1,121 @@ +package com.sonic.bear.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.github.benmanes.caffeine.cache.RemovalListener; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.sonic.common.auth.domains.Session; +import com.sonic.bear.domain.entity.UserSession; +import com.sonic.bear.service.SessionCacheService; +import com.sonic.bear.service.UserSessionService; +import com.sonic.common.auth.domains.SessionStatusEnum; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.Optional; +import java.util.Random; + +/** + * @author coder + */ +@Service +public class SessionCacheServiceImpl implements SessionCacheService, InitializingBean { + /** + * 默认redis session缓存时间 8小时 + */ + private static volatile Duration DEFAULT_REDIS_SESSION_CACHE_SECONDS = Duration.ofSeconds(8 * 60 * 60); + + /** + * 默认local session缓存时间 2小时 + */ + private static volatile Duration DEFAULT_LOCAL_SESSION_CACHE_SECONDS = Duration.ofSeconds(2 * 60 * 60); + + private static volatile Random RANDOM_MINUTE_IN_HOURS = new Random(60); + + private LoadingCache sessionCache; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private UserSessionService userSessionService; + + private String sessionRedisKey(String token) { + return String.format("bs:sso:session:%s", token); + } + + @Override + public Optional get(String token) { + // 通过get获取缓存,如果没有,则异步执行load从redis加载数据。即,多线程情况,可能会加载到旧值 + return Optional.ofNullable(sessionCache.get(token)); + } + + @Override + public void cache(String token, Session session) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(token)); + Preconditions.checkNotNull(session); + + // 削峰,避免短时间内redis大量缓存过期,对数据库造成压力,所以redis过期时间随机分布在60分钟内 + Duration timeout = DEFAULT_REDIS_SESSION_CACHE_SECONDS.plusMinutes(RANDOM_MINUTE_IN_HOURS.nextInt(60)); + redisTemplate.opsForValue().set(sessionRedisKey(token), JSON.toJSONString(session), timeout); + sessionCache.put(token, session); + } + + @Override + public void localCache(String token, Session session) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(token)); + Preconditions.checkNotNull(session); + + sessionCache.put(token, session); + } + + @Override + public void invalidate(String token) { + redisTemplate.delete(sessionRedisKey(token)); + sessionCache.invalidate(token); + } + + @Override + public void afterPropertiesSet() { + sessionCache = Caffeine.newBuilder() + .expireAfterAccess(DEFAULT_LOCAL_SESSION_CACHE_SECONDS) + // TODO: 目前活跃用户不到1w,这里最大按50倍计算,设置50w缓存数量 + .maximumSize(500_000) + .initialCapacity(500_000 / 50) + .recordStats() + .removalListener((RemovalListener) (key, value, cause) -> { + // 缓存失效时,把数据更新回db + if (value == null) { + return; + } + UserSession userSession = userSessionService.get(key).orElse(null); + if (userSession == null || !userSession.getToken().equals(key)) { + return; + } + + UserSession toUpdate = UserSession.builder() + .expireTime(value.getExpireTime()) + .lastAccessTime(value.getLastAccessTime()) + .status(value.isExpired() ? SessionStatusEnum.EXPIRED : userSession.getStatus()).build(); + userSessionService.update(toUpdate, Wrappers.lambdaQuery() + .eq(UserSession::getId, userSession.getId()) + .eq(UserSession::getStatus, userSession.getStatus())); + }) + .build(new CacheLoader() { + @Nullable + @Override + public Session load(@NonNull String key) { + return JSON.parseObject(redisTemplate.opsForValue().get(sessionRedisKey(key)), Session.class); + } + }); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/SessionExistVerifyCacheServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/SessionExistVerifyCacheServiceImpl.java new file mode 100644 index 0000000..7b3d5a6 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/SessionExistVerifyCacheServiceImpl.java @@ -0,0 +1,105 @@ +package com.sonic.bear.service.impl; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.sonic.bear.service.SessionExistVerifyCacheService; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * @Author code + * @Description 用户Token存在性验证缓存,防止穿透 + * @Date 2022/11/14 19:03 + * @Version 1.0 + */ +@Slf4j +@Service +public class SessionExistVerifyCacheServiceImpl implements SessionExistVerifyCacheService, InitializingBean { + + /** + * 默认local exist token缓存时间 2分钟 + */ + private static volatile Duration DEFAULT_LOCAL_TOKEN_CACHE_SECONDS = Duration.ofSeconds(2 * 60); + + /** + * 存在的token缓存 + */ + private LoadingCache sessionExistCache; + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 存在的token的redis的缓存key + * @param token + * @return + */ + private String sessionExistRedisKey(String token) { + return String.format("bs:sso:exist:%s", token); + } + + @Override + public Optional get(String token) { + // 通过get获取缓存,如果没有,则异步执行load从redis加载数据。即,多线程情况,可能会加载到旧值 + return Optional.ofNullable(sessionExistCache.get(token)); + } + + @Override + public boolean exist(String token) { + //将异常cache住,并返回true,继续往下执行 + Optional value = get(token); + return value.isPresent(); + } + + @Override + public void cache(String token, Long expTime) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(token)); + // 缓存指定的时间 + redisTemplate.opsForValue().set(sessionExistRedisKey(token), "1", expTime, TimeUnit.SECONDS); + //将缓存写入本地 + sessionExistCache.put(token, "1"); + } + + @Override + public void initCache(String token, Long expTime) { + // 缓存指定的时间 + redisTemplate.opsForValue().set(sessionExistRedisKey(token), "1", expTime, TimeUnit.SECONDS); + } + + @Override + public void clearCache(String token) { + //清理掉本地缓存 + sessionExistCache.invalidate(token); + //删除掉redis的缓存 + redisTemplate.delete(sessionExistRedisKey(token)); + } + + @Override + public void afterPropertiesSet() throws Exception { + sessionExistCache = Caffeine.newBuilder() + .expireAfterAccess(DEFAULT_LOCAL_TOKEN_CACHE_SECONDS) + //TODO: 统计了30天的活跃token数据量为30万,这儿先设置最大为30万,大概会占用60M左右的内存【这儿还可以放小一点,因为这儿只会存储存在的在本地缓存中】 + .maximumSize(300_000) + .initialCapacity(300_000 / 30) + .recordStats() + .build(new CacheLoader() { + @Nullable + @Override + public String load(@NonNull String key) { + return redisTemplate.opsForValue().get(sessionExistRedisKey(key)); + } + }); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/SessionKickOutCacheServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/SessionKickOutCacheServiceImpl.java new file mode 100644 index 0000000..4220384 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/SessionKickOutCacheServiceImpl.java @@ -0,0 +1,76 @@ +package com.sonic.bear.service.impl; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.sonic.bear.service.SessionKickOutCacheService; +import com.sonic.bear.utils.RedisKeyUtils; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * @author coder + */ +@Service +public class SessionKickOutCacheServiceImpl implements SessionKickOutCacheService, InitializingBean { + + /** + * 默认local kickOut token缓存时间 2小时 + */ + private static volatile Duration DEFAULT_LOCAL_SESSION_CACHE_SECONDS = Duration.ofSeconds(2 * 60 * 60); + + private LoadingCache sessionKickOutCache; + + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + + + @Override + public Optional get(String token) { + // 通过get获取缓存,如果没有,则异步执行load从redis加载数据。即,多线程情况,可能会加载到旧值 + return Optional.ofNullable(sessionKickOutCache.get(token)); + } + + @Override + public void cache(String token) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(token)); + // 缓存 12 小时(只要缓存的时间 > token在redis中存储的时间即可) + redisTemplate.opsForValue().set(redisKeyUtils.sessionKickOutKey(token), "1", 12, TimeUnit.HOURS); + sessionKickOutCache.put(token, "1"); + } + + @Override + public void invalidate(String token) { + redisTemplate.delete(redisKeyUtils.sessionKickOutKey(token)); + sessionKickOutCache.invalidate(token); + } + + @Override + public void afterPropertiesSet() { + sessionKickOutCache = Caffeine.newBuilder() + .expireAfterAccess(DEFAULT_LOCAL_SESSION_CACHE_SECONDS) + // 目前活跃用户不到1w,这里最大按50倍计算,设置50w缓存数量 + .maximumSize(500_000) + .initialCapacity(500_000 / 50) + .recordStats() + .build(new CacheLoader() { + @Nullable + @Override + public String load(@NonNull String key) { + return redisTemplate.opsForValue().get(redisKeyUtils.sessionKickOutKey(key)); + } + }); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserAccessLogServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserAccessLogServiceImpl.java new file mode 100644 index 0000000..3cd54e7 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserAccessLogServiceImpl.java @@ -0,0 +1,82 @@ +package com.sonic.bear.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.sonic.bear.dao.UserAccessLogDao; +import com.sonic.bear.domain.entity.UserAccessLog; +import com.sonic.bear.service.UserAccessLogService; +import com.sonic.bear.utils.RedisKeyUtils; +import com.sonic.common.utils.RedisLock; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +/** + * @author code + */ +@Slf4j +@Service +public class UserAccessLogServiceImpl extends ServiceImpl implements UserAccessLogService, InitializingBean { + + private static volatile Duration DEFAULT_LOCAL_CACHE_SECONDS = Duration.ofSeconds(3 * 60 * 60); + + @Autowired + private RedisTemplate redisTemplate; + + private LoadingCache isAddLogStatusCache; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @Override + public void addLog(Long userId, String clientCode) { + try { + String status = isAddLogStatusCache.get(userId); + if(StringUtils.isNotEmpty(status)) { + return; + } + //加锁进行保存操作 + RedisLock redisLock = new RedisLock(redisKeyUtils.addAccessLogLockKey(userId), redisWrapper); + redisLock.tryAcquireRun(() -> { + //添加数据到日志表中 + save(UserAccessLog.builder().userId(userId).clientCode(clientCode).accessTime(LocalDateTime.now()).build()); + return true; + }); + //写入redis缓存中 24小时过期 + redisTemplate.opsForValue().set(redisKeyUtils.addAccessLogKey(userId), "1", 6, TimeUnit.HOURS); + } catch (Exception e) { + log.error("===> addLog error : ", e); + } + + } + + @Override + public void afterPropertiesSet() throws Exception { + isAddLogStatusCache = Caffeine.newBuilder() + .expireAfterWrite(DEFAULT_LOCAL_CACHE_SECONDS) + // 目前活跃用户不到1w,这里最大按50倍计算,设置50w缓存数量 + .maximumSize(500_000) + .initialCapacity(500_000 / 50) + .recordStats() + .build(new CacheLoader() { + @Nullable + @Override + public String load(@NonNull Long key) { + return redisTemplate.opsForValue().get(redisKeyUtils.addAccessLogKey(key)); + } + }); + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserCommonlyUsedLogServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserCommonlyUsedLogServiceImpl.java new file mode 100644 index 0000000..9fbd4a4 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserCommonlyUsedLogServiceImpl.java @@ -0,0 +1,58 @@ +package com.sonic.bear.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.bear.dao.UserCommonlyUsedLogDao; +import com.sonic.bear.domain.entity.UserCommonlyUsedLog; +import com.sonic.bear.service.UserCommonlyUsedLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author code + */ +@Slf4j +@Service +public class UserCommonlyUsedLogServiceImpl extends ServiceImpl implements UserCommonlyUsedLogService { + + @Override + public void addData(Long userId, Integer dataType, String dataValue) { + try { + //查询数据库,再写入数据 + UserCommonlyUsedLog queryUserCommonlyUsedLog = getOne(Wrappers.lambdaQuery() + .select(UserCommonlyUsedLog::getId, UserCommonlyUsedLog::getUseCount, UserCommonlyUsedLog::getDataValue) + .eq(UserCommonlyUsedLog::getUserId, userId) + .eq(UserCommonlyUsedLog::getDataType, dataType) + .eq(UserCommonlyUsedLog::getDataValue, dataValue)); + if(queryUserCommonlyUsedLog != null) { + //更新数据 + queryUserCommonlyUsedLog.setUseCount(queryUserCommonlyUsedLog.getUseCount() + 1); + updateById(queryUserCommonlyUsedLog); + return; + } + UserCommonlyUsedLog userCommonlyUsedLog = new UserCommonlyUsedLog(); + userCommonlyUsedLog.setUserId(userId); + userCommonlyUsedLog.setDataType(dataType); + userCommonlyUsedLog.setDataValue(dataValue); + userCommonlyUsedLog.setUseCount(1); + userCommonlyUsedLog.setCreateTime(LocalDateTime.now()); + userCommonlyUsedLog.setEditTime(LocalDateTime.now()); + save(userCommonlyUsedLog); + } catch (Exception e) { + log.error("===> UserCommonlyUsedLog addData error : ", e); + } + } + + @Override + public List queryData(List userIdList, List dataValueList) { + List list = list(Wrappers.lambdaQuery() + .select(UserCommonlyUsedLog::getUserId, UserCommonlyUsedLog::getDataType, UserCommonlyUsedLog::getDataValue) + .in(UserCommonlyUsedLog::getUserId, userIdList) + .in(UserCommonlyUsedLog::getDataValue, dataValueList)); + return list; + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserNicknamePoolServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserNicknamePoolServiceImpl.java new file mode 100644 index 0000000..ae6e8ab --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserNicknamePoolServiceImpl.java @@ -0,0 +1,61 @@ +package com.sonic.bear.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.bear.dao.UserNicknamePoolDao; +import com.sonic.bear.domain.entity.UserNicknamePool; +import com.sonic.bear.domain.output.NicknameOutput; +import com.sonic.bear.lib.enums.OptTypeEnum; +import com.sonic.bear.service.UserNicknamePoolService; +import com.sonic.bear.utils.BeanConver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +/** + * @author code + */ +@Slf4j +@Service +public class UserNicknamePoolServiceImpl extends ServiceImpl implements UserNicknamePoolService { + + + @Transactional(rollbackFor = Exception.class) + @Override + public void syncPool(Long userId, Integer userType, String nickname, String optType) { + if(StringUtils.isEmpty(nickname)) { + return; + } + if(OptTypeEnum.ADD.name().equals(optType)) { + save(UserNicknamePool.builder().userId(userId).userType(userType).nickname(nickname).createTime(LocalDateTime.now()).build()); + } else if (OptTypeEnum.DEL.name().equals(optType)) { + //删除历史数据 + remove(Wrappers.lambdaQuery().eq(UserNicknamePool::getUserId, userId).eq(UserNicknamePool::getUserType, userType)); + } else if (OptTypeEnum.UPD.name().equals(optType)) { + //先删除历史数据 + remove(Wrappers.lambdaQuery().eq(UserNicknamePool::getUserId, userId).eq(UserNicknamePool::getUserType, userType)); + //再写入数据 + save(UserNicknamePool.builder().userId(userId).userType(userType).nickname(nickname).createTime(LocalDateTime.now()).build()); + } + } + + @Override + public List batchGetNickname(List userIdList) { + List list = list(Wrappers.lambdaQuery() + .select(UserNicknamePool::getUserId, UserNicknamePool::getNickname) + .in(UserNicknamePool::getUserId, userIdList)); + return BeanConver.copeList(list, NicknameOutput.class); + } + + @Override + public boolean existCheck(Long userId, String nickname) { + int count = count(Wrappers.lambdaQuery().eq(UserNicknamePool::getNickname, nickname).ne(userId != null, UserNicknamePool::getUserId, userId)); + return count > 0; + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserSearchServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserSearchServiceImpl.java new file mode 100644 index 0000000..eac8266 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserSearchServiceImpl.java @@ -0,0 +1,69 @@ + +package com.sonic.bear.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.bear.domain.entity.User; +import com.sonic.bear.domain.entity.UserThird; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.bear.lib.output.UserInfoListOutput; +import com.sonic.bear.service.UserSearchService; +import com.sonic.bear.service.UserService; +import com.sonic.bear.service.UserThirdService; +import com.sonic.bear.utils.BeanConver; +import com.sonic.lion.lib.client.SubscribeClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author code + */ +@Slf4j +@Service +public class UserSearchServiceImpl implements UserSearchService { + + @Autowired + private UserService userService; + @Autowired + private UserThirdService userThirdService; + @Autowired + private SubscribeClient subscribeClient; + + @Override + public List batchGetUserInfo(List userIdList) { + List list = userService.list(Wrappers.lambdaQuery() + .select(User::getUserId, User::getIdCard, User::getHeadImage, User::getNickname, User::getSex) + .in(User::getUserId, userIdList)); + return BeanConver.copeList(list, UserInfoListOutput.class); + } + + @Override + public BaseUserInfoOutput baseUserInfo(Long userId) { + User user = userService.getOne(Wrappers.lambdaQuery() + .select(User::getId, User::getUserId, User::getIdCard, User::getHeadImage, User::getNickname, User::getSex, User::getBirthday, User::getLastAccessTime) + .eq(User::getUserId, userId)); + //根据最后一次访问时间的间隔来判断是否需要更新最后访问时间(每5分钟更新一次,跨天的话首次访问时同步更新) + if(user.getLastAccessTime() == null + || user.getLastAccessTime().plusMinutes(5).isBefore(LocalDateTime.now()) + || user.getLastAccessTime().getDayOfMonth() != LocalDateTime.now().getDayOfMonth()) { + userService.update(Wrappers.lambdaUpdate().set(User::getLastAccessTime, LocalDateTime.now()).eq(User::getId, user.getId())); + } + BaseUserInfoOutput output = BeanConver.copeBean(user, BaseUserInfoOutput.class); + output.setCpUserInfo(user.getSex() == null || StringUtils.isEmpty(user.getNickname()) || user.getBirthday() == null); + //查询三方信息 + UserThird userThird = userThirdService.getOne(Wrappers.lambdaQuery() + .select(UserThird::getThirdType, UserThird::getNickname, UserThird::getEmail) + .eq(UserThird::getUserId, userId)); + output.setThirdType(userThird == null ? null : userThird.getThirdType()); + output.setThirdNickname(userThird == null ? null : userThird.getNickname()); + output.setThirdEmail(userThird == null ? null : userThird.getEmail()); + output.setIsMember(subscribeClient.queryUserIsSubscribe(userId)); + return output; + } + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..b6921f5 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserServiceImpl.java @@ -0,0 +1,121 @@ +package com.sonic.bear.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Sequence; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.bear.dao.UserDao; +import com.sonic.bear.domain.entity.User; +import com.sonic.bear.domain.entity.UserInfoBo; +import com.sonic.bear.domain.entity.UserThird; +import com.sonic.bear.enums.ThirdTypeEnum; +import com.sonic.bear.enums.ToastResultCode; +import com.sonic.bear.service.CommonMessageService; +import com.sonic.bear.service.CommonSendMqService; +import com.sonic.bear.service.UserService; +import com.sonic.bear.service.UserThirdService; +import com.sonic.bear.utils.BeanConver; +import com.sonic.bear.utils.IdCardGenerator; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.pigeon.lib.client.ImUserClient; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import com.sonic.pigeon.lib.input.CreateImUserInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * @author code + */ +@Slf4j +@Service +public class UserServiceImpl extends ServiceImpl implements UserService { + + @Autowired + private UserDao userDao; + @Autowired + private UserThirdService userThirdService; + @Autowired + private IdCardGenerator idCardGenerator; + + /** + * 构造ID生成器的对象 + */ + private final Sequence sequence = new Sequence(); + + @Autowired + private CommonSendMqService commonSendMqService; + + @Transactional(rollbackFor = Exception.class) + @Override + public Long thirdLoginOrRegister(ThirdTypeEnum thirdType, String openId, String email, String nickname) { + UserThird userThird = userThirdService.queryByOpenId(thirdType, openId); + if(userThird == null) { + //直接走注册的流程,并快速返回 + Long userId = sequence.nextId(); + registerThirdUser(userId, thirdType, openId, email, nickname); + //用户注册后发送MQ + commonSendMqService.userCreatedSendMq(userId); + return userId; + } + User user = getOne(Wrappers.lambdaQuery() + .eq(User::getIsDelete, false) + .eq(User::getUserId, userThird.getUserId())); + //判断用户是否存在 + ToastResultCode.USER_NOT_EXIST.check(user == null); + //判断账号状态 + ToastResultCode.USER_FROZEN.check(user.getAccountStatus() != 0); + return user.getUserId(); + } + + /** + * 三方用户注册 + * @param userId + * @param thirdType + * @param openId + * @param email + * @param nickname + */ + private UserThird registerThirdUser(Long userId, ThirdTypeEnum thirdType, String openId, String email, String nickname) { + User user = new User(); + user.setUserId(userId); + //idCard的生成逻辑 + user.setIdCard(idCardGenerator.generateIdCard()); + user.setAccountStatus(0); + user.setSex(0); + user.setHeadImage("https://hhb.crushlevel.ai/static/img/dfhig.jpg"); + user.setLastAccessTime(LocalDateTime.now()); + user.setIsDelete(false); + user.setCreateTime(LocalDateTime.now()); + user.setEditId(userId); + user.setEditTime(LocalDateTime.now()); + userDao.insert(user); + UserThird userThird = new UserThird(); + userThird.setUserId(userId); + userThird.setThirdType(thirdType.name()); + userThird.setOpenId(openId); + userThird.setNickname(nickname); + userThird.setEmail(email); + userThird.setIsDelete(false); + userThird.setCreateTime(LocalDateTime.now()); + userThird.setEditId(userId); + userThird.setEditTime(LocalDateTime.now()); + userThirdService.save(userThird); + log.info("===> 用户{}注册成功", userId); + return userThird; + } + + @Override + public UserInfoBo queryUserInfo(Long userId) { + User user = getOne(Wrappers.lambdaQuery() + .eq(User::getIsDelete, false) + .eq(User::getUserId, userId)); + //判断用户是否存在 + ToastResultCode.USER_NOT_EXIST.check(user == null); + //判断账号状态 + ToastResultCode.USER_FROZEN.check(user.getAccountStatus() != 0); + return BeanConver.copeBean(user, UserInfoBo.class); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserSessionServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserSessionServiceImpl.java new file mode 100644 index 0000000..e1b1cfc --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserSessionServiceImpl.java @@ -0,0 +1,324 @@ +package com.sonic.bear.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.bear.dao.UserSessionDao; +import com.sonic.bear.domain.entity.AppClient; +import com.sonic.bear.domain.entity.UserInfoBo; +import com.sonic.bear.domain.entity.UserSession; +import com.sonic.bear.domain.input.KickOutInput; +import com.sonic.bear.domain.input.LogoutInput; +import com.sonic.bear.enums.ToastResultCode; +import com.sonic.bear.service.*; +import com.sonic.bear.utils.RedisKeyUtils; +import com.sonic.bear.utils.UUIDGenerator; +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.auth.domains.SessionStatusEnum; +import com.sonic.common.utils.RedisLock; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * @author coder + */ +@Slf4j +@Service +public class UserSessionServiceImpl extends ServiceImpl implements UserSessionService { + + @Autowired + private UserService userService; + @Autowired + private UserSessionDao userSessionDao; + @Autowired + private SessionCacheService sessionCacheService; + @Autowired + private SessionKickOutCacheService sessionKickOutCacheService; + @Autowired + private AppClientService appClientService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private AppRuntime appRuntime; + @Autowired + private UserAccessLogService userAccessLogService; + @Autowired + private SessionExistVerifyCacheService sessionExistVerifyCacheService; + @Autowired + private UserCommonlyUsedLogService userCommonlyUsedLogService; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @Transactional(rollbackFor = Throwable.class) + @Override + public Session createSession(CreateSessionReq param) { + //获取用户信息,判断用户是否存在能否进行登录操作 + UserInfoBo userInfoBo = userService.queryUserInfo(param.getUserId()); + ToastResultCode.USER_NOT_EXIST.check(userInfoBo == null); + //创建登录授权凭证 + return createSession(param, userInfoBo); + } + + + /** + * 将历史登录凭证进行失效 + */ + private void expireSessionWhenMobileDeviceOnLine(Long userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(UserSession::getStatus, SessionStatusEnum.ENABLED) + .eq(UserSession::getUserId, userId); + UserSession userSession = userSessionDao.selectOne(queryWrapper); + if (userSession != null) { + sessionCacheService.invalidate(userSession.getToken()); + userSessionDao.expiredSessionV2(userId); + //将无效的token添加到kickOut中 用户进行数据排除 + sessionKickOutCacheService.cache(userSession.getToken()); + } + } + + private Session insertSession(CreateSessionReq param, AppClient appClient, UserInfoBo userInfoBo, LocalDateTime now) { + UserSession userSession = UserSession.builder() + .userId(param.getUserId()) + .accountType(userInfoBo.getAccountTypeEnum()) + .token(UUIDGenerator.generate32Uuid()) + .clientCode(param.getClientCode()) + .loginType(param.getLoginType()) + .status(SessionStatusEnum.ENABLED) + .userAgent(param.getUserAgent()) + .deviceId(param.getDeviceId()) + .ip(param.getIp()) + .createTime(now) + .lastAccessTime(now) + .expireTime(generateExpiredTime(appClient.getSessionExpired(), appClient.getSessionAlive(), now)) + .build(); + baseMapper.insert(userSession); + + //添加用户常用设备日志数据 IP地址 设备ID + userCommonlyUsedLogService.addData(param.getUserId(), 1, param.getIp()); + userCommonlyUsedLogService.addData(param.getUserId(), 2, param.getDeviceId()); + + Session session = convertToSession(userSession); + session.setEndpoint(userSession.getClientCode()); + sessionCacheService.cache(session.getToken(), session); + //将缓存数据写入是否存在的验证缓存中以防止穿透数据库 + sessionExistVerifyCacheService.cache(session.getToken(), appClient.getSessionExpired().longValue() * 60); + return session; + } + + + private void updateInvalidSession(CreateSessionReq param, LocalDateTime now) { + String clientCode = param.getClientCode(); + Long userId = param.getUserId(); + List userSessionList = baseMapper.selectList( + Wrappers.lambdaQuery(UserSession.builder() + .userId(userId) + .clientCode(clientCode) + .status(SessionStatusEnum.ENABLED).build())); + if (CollectionUtils.isEmpty(userSessionList)) { + return; + } + for (UserSession userSession : userSessionList) { + sessionCacheService.invalidate(userSession.getToken()); + if (userSession.isExpired()) { + userSession.setStatus(SessionStatusEnum.EXPIRED); + } else { + userSession.setStatus(SessionStatusEnum.LOGOUT).setExpireTime(now); + } + update(userSession, Wrappers.lambdaQuery() + .eq(UserSession::getId, userSession.getId()) + .eq(UserSession::getStatus, SessionStatusEnum.ENABLED)); + //将无效的token添加到kickOut中 用户进行数据排除 + sessionKickOutCacheService.cache(userSession.getToken()); + //将已经存在的token删除掉 + sessionExistVerifyCacheService.clearCache(userSession.getToken()); + } + } + + + @Override + public Session touchSession(String token) { + LocalDateTime now = LocalDateTime.now(); + Optional sessionOpt = extendCacheSessionValidity(token, now); + if (sessionOpt.isPresent()) { + Session session = sessionOpt.get(); + //判断大小写必须完全保持一致(防止数据库忽略大小写的问题发生) + ToastResultCode.SESSION_INVALID.check(!token.equals(session.getToken())); + //写入访问日志数据到记录表中 + userAccessLogService.addLog(session.getUserId(), session.getEndpoint()); + return session; + } + + //查询数据库验证token的有效性 + Optional userSessionOptional = get(token); + ToastResultCode.SESSION_INVALID.check(!userSessionOptional.isPresent()); + UserSession userSession = userSessionOptional.get(); + ToastResultCode.SESSION_INVALID.check(!userSession.isEnabled()); + //判断大小写的问题 + ToastResultCode.SESSION_INVALID.check(!token.equals(userSession.getToken())); + + //写入访问日志数据到记录表中 + userAccessLogService.addLog(userSession.getUserId(), userSession.getClientCode()); + + Optional appClientOptional = appClientService.getByCode(userSession.getClientCode()); + ToastResultCode.APP_CLIENT_NOT_EXIST.check(!appClientOptional.isPresent()); + AppClient appClient = appClientOptional.get(); + // 如果已经过期,更新session状态,并直接抛出异常提示 + if (userSession.isExpired() || userSession.isOverMaxAlive(appClient.getSessionAlive())) { + baseMapper.updateById(UserSession.builder() + .id(userSession.getId()) + .status(SessionStatusEnum.EXPIRED) + .lastAccessTime(LocalDateTime.now()).build()); + //TODO 清理掉缓存数据 + sessionExistVerifyCacheService.clearCache(userSession.getToken()); + ToastResultCode.SESSION_EXPIRED.check(true); + } + + //过期时间 之前是用当前时间来更新的,现在换成创建时间来进行判断 + LocalDateTime expireTime = generateExpiredTime(appClient.getSessionExpired(), appClient.getSessionAlive(), userSession.getCreateTime()); + // 延期session + baseMapper.updateById(UserSession.builder() + .id(userSession.getId()) + .expireTime(expireTime) + .lastAccessTime(now).build()); + userSession.setLastAccessTime(now); + userSession.setExpireTime(expireTime); + Session session = convertToSession(userSession); + session.setToken(token); + sessionCacheService.cache(token, session); + return session; + } + + private Optional extendCacheSessionValidity(String token, LocalDateTime now) { + Optional sessionOpt = sessionCacheService.get(token); + if (!sessionOpt.isPresent()) { + return Optional.empty(); + } + Session session = sessionOpt.get(); + Optional appClientOptional = appClientService.getByCode(session.getEndpoint()); + ToastResultCode.APP_CLIENT_NOT_EXIST.check(!appClientOptional.isPresent()); + AppClient appClient = appClientOptional.get(); + //校验用户是否 已经被kickOut了,如果被踢下线了则直接返回异常 + Optional sessionKickOutOpt = sessionKickOutCacheService.get(token); + if (session.isExpired() || session.isDisabled() || session.isOverMaxAlive(appClient.getSessionAlive()) || sessionKickOutOpt.isPresent()) { + sessionCacheService.invalidate(token); + //清理掉缓存数据 + sessionExistVerifyCacheService.clearCache(session.getToken()); + ToastResultCode.SESSION_EXPIRED.check(true); + } + session.setLastAccessTime(now); + session.setExpireTime(generateExpiredTime(appClient.getSessionExpired(), appClient.getSessionAlive(), session.getCreateTime())); + sessionCacheService.localCache(token, session); + return sessionOpt; + } + + private Session createSession(CreateSessionReq param, UserInfoBo userInfoBo) { + LocalDateTime now = LocalDateTime.now(); + Optional clientOpt = appClientService.getByCode(param.getClientCode()); + ToastResultCode.APP_CLIENT_NOT_EXIST.check(!clientOpt.isPresent()); + AppClient appClient = clientOpt.get(); + ToastResultCode.APP_CLIENT_NOT_ALLOW.check(!appClient.allowUser(userInfoBo.getAccountTypeEnum())); + + // 用户登陆创建 session 加锁 + RedisLock redisLock = new RedisLock(redisKeyUtils.sessionCreateLockKey(userInfoBo.getUserId()), redisWrapper); + return redisLock.tryAcquireRun(() -> { + // 更新无效的session + updateInvalidSession(param, now); + // 先让已经登陆了的设备 失效 + expireSessionWhenMobileDeviceOnLine(param.getUserId()); + // 插入新的有效session + return insertSession(param, appClient, userInfoBo, now); + }); + } + + @Transactional(rollbackFor = Throwable.class) + @Override + public void logout(LogoutInput input) { + UserSession session = get(input.getToken()).orElse(null); + if (session == null || session.isDisabled()) { + return; + } + this.updateById(UserSession.builder() + .id(session.getId()) + .status(SessionStatusEnum.LOGOUT) + .lastAccessTime(LocalDateTime.now()) + .build()); + sessionCacheService.invalidate(session.getToken()); + //将用户的token放入kickOut中 + sessionKickOutCacheService.cache(session.getToken()); + //清理掉缓存数据 + sessionExistVerifyCacheService.clearCache(session.getToken()); + } + + @Override + public void kickOut(KickOutInput input) { + List sessions = this.list(Wrappers.lambdaQuery() + .eq(UserSession::getUserId, input.getUserId()) + .eq(UserSession::getStatus, SessionStatusEnum.ENABLED)); + if (sessions.isEmpty()) { + return; + } + List toUpdateEnableSessions = sessions.stream().filter(UserSession::isEnabled).map(session -> { + //将被踢出去的用户token存放到kickOut的缓存中 + sessionKickOutCacheService.cache(session.getToken()); + //清理掉缓存数据 + sessionExistVerifyCacheService.clearCache(session.getToken()); + sessionCacheService.invalidate(session.getToken()); + return UserSession.builder() + .id(session.getId()) + .status(SessionStatusEnum.KICK_OUT) + .lastAccessTime(LocalDateTime.now()) + .build(); + }).collect(Collectors.toList()); + if (toUpdateEnableSessions.isEmpty()) { + return; + } + this.updateBatchById(toUpdateEnableSessions); + } + + @Override + public Optional get(String token) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(UserSession.builder() + .token(token).build()); + return Optional.ofNullable(getOne(wrapper)); + } + + /** + * 过期时间不能超过 client 配置的最大存活时间 + * + * @param sessionExpired session多久过期,单位分钟 + * @param sessionAlive session存活时间,单位分钟 + * @param createTime 创建时间 + * @return 有效的过期时间 + */ + private LocalDateTime generateExpiredTime(Integer sessionExpired, Integer sessionAlive, LocalDateTime createTime) { + long maxAliveTimeMilli = createTime.plusMinutes(sessionAlive).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long expiredTimeMilli = LocalDateTime.now().plusMinutes(sessionExpired).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long maxAlive = Math.min(expiredTimeMilli, maxAliveTimeMilli); + return Instant.ofEpochMilli(maxAlive).atZone(ZoneId.systemDefault()).toLocalDateTime(); + } + + /** + * 数据对象转换 + * @param userSession + * @return + */ + Session convertToSession(UserSession userSession) { + Session session = new Session(); + session.setEndpoint(userSession.getClientCode()); + BeanUtils.copyProperties(userSession, session, "extra"); + return session; + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserSetServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserSetServiceImpl.java new file mode 100644 index 0000000..5971b3a --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserSetServiceImpl.java @@ -0,0 +1,125 @@ + +package com.sonic.bear.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.bear.domain.entity.User; +import com.sonic.bear.domain.entity.UserThird; +import com.sonic.bear.domain.input.KickOutInput; +import com.sonic.bear.lib.enums.OptTypeEnum; +import com.sonic.bear.lib.enums.UserTypeEnum; +import com.sonic.bear.lib.input.CompleteUserInfoInput; +import com.sonic.bear.lib.input.EditUserInfoInput; +import com.sonic.bear.service.*; +import com.sonic.pigeon.lib.client.ImUserClient; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import com.sonic.pigeon.lib.input.EditImUserInput; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.baomidou.mybatisplus.core.toolkit.Wrappers.update; + +/** + * @author code + */ +@Slf4j +@Service +public class UserSetServiceImpl implements UserSetService { + + @Autowired + private UserService userService; + @Autowired + private UserThirdService userThirdService; + @Autowired + private UserSessionService userSessionService; + @Autowired + private UserNicknamePoolService userNicknamePoolService; + @Autowired + private ImUserClient imUserClient; + + @Transactional(rollbackFor = Exception.class) + @Override + public void completeUserInfo(CompleteUserInfoInput input) { + //查询用户基础信息 + User user = userService.getOne(Wrappers.lambdaQuery().eq(User::getUserId, input.getUserId())); + //更新用户基础信息 + userService.update(Wrappers.lambdaUpdate() + .set(StringUtils.isNotEmpty(input.getHeadImage()), User::getHeadImage, input.getHeadImage()) + .set(StringUtils.isNotEmpty(input.getNickname()), User::getNickname, input.getNickname()) + .set(input.getSex() != null, User::getSex, input.getSex()) + .set(input.getBirthday() != null, User::getBirthday, input.getBirthday()) + .eq(User::getUserId, input.getUserId())); + if(user == null) { + //将数据添加到数据池 + userNicknamePoolService.syncPool(input.getUserId(), UserTypeEnum.USER.getCode(), input.getNickname(), OptTypeEnum.ADD.name()); + } else { + //将数据添加到数据池 + userNicknamePoolService.syncPool(input.getUserId(), UserTypeEnum.USER.getCode(), input.getNickname(), OptTypeEnum.UPD.name()); + } + //有数据变更则需要更新im用户基础信息 + if((StringUtils.isNotEmpty(input.getNickname()) && !input.getNickname().equals(user.getNickname())) || + (StringUtils.isNotEmpty(input.getHeadImage()) && !input.getHeadImage().equals(user.getHeadImage()))) { + User querUser = userService.getOne(Wrappers.lambdaQuery().eq(User::getUserId, input.getUserId())); + //更新im基础信息 + EditImUserInput editImUserInput = new EditImUserInput(); + editImUserInput.setImUserType(ImUserTypeEnum.u); + editImUserInput.setUserId(querUser.getUserId()); + editImUserInput.setNickname(querUser.getNickname()); + editImUserInput.setHeadImage(querUser.getHeadImage()); + //编辑im基础信息 + imUserClient.editImUser(editImUserInput); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void editUserInfo(EditUserInfoInput input) { + //查询用户基础信息 + User user = userService.getOne(Wrappers.lambdaQuery().eq(User::getUserId, input.getUserId())); + //更新用户基础信息 + userService.update(Wrappers.lambdaUpdate() + .set(StringUtils.isNotEmpty(input.getHeadImage()), User::getHeadImage, input.getHeadImage()) + .set(StringUtils.isNotEmpty(input.getNickname()), User::getNickname, input.getNickname()) + .set(input.getBirthday() != null, User::getBirthday, input.getBirthday()) + .eq(User::getUserId, input.getUserId())); + //用户昵称有变更 + if(!user.getNickname().equals(input.getNickname())) { + //将数据添加到数据池 + userNicknamePoolService.syncPool(input.getUserId(), UserTypeEnum.USER.getCode(), input.getNickname(), OptTypeEnum.UPD.name()); + } + //有数据变更则需要更新im用户基础信息 + if(!user.getNickname().equals(input.getNickname()) || !user.getHeadImage().equals(input.getHeadImage())) { + User querUser = userService.getOne(Wrappers.lambdaQuery().eq(User::getUserId, input.getUserId())); + //更新im基础信息 + EditImUserInput editImUserInput = new EditImUserInput(); + editImUserInput.setImUserType(ImUserTypeEnum.u); + editImUserInput.setUserId(querUser.getUserId()); + editImUserInput.setNickname(querUser.getNickname()); + editImUserInput.setHeadImage(querUser.getHeadImage()); + //编辑im基础信息 + imUserClient.editImUser(editImUserInput); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void delAccount(Long userId) { + //删除基础账号 + update(Wrappers.lambdaUpdate() + .set(User::getIsDelete, true) + .eq(User::getUserId, userId)); + //删除三方账号 + userThirdService.update(Wrappers.lambdaUpdate() + .set(UserThird::getIsDelete, true) + .eq(UserThird::getUserId, userId)); + //将用户踢下线 + KickOutInput input = new KickOutInput(); + input.setUserId(userId); + userSessionService.kickOut(input); + //将数据从数据池中删除 + userNicknamePoolService.syncPool(input.getUserId(), UserTypeEnum.USER.getCode(), null, OptTypeEnum.DEL.name()); + } + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserThirdServiceImpl.java b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserThirdServiceImpl.java new file mode 100644 index 0000000..e509159 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/service/impl/UserThirdServiceImpl.java @@ -0,0 +1,28 @@ +package com.sonic.bear.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.bear.dao.UserThirdDao; +import com.sonic.bear.domain.entity.UserThird; +import com.sonic.bear.enums.ThirdTypeEnum; +import com.sonic.bear.service.UserThirdService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author code + */ +@Slf4j +@Service +public class UserThirdServiceImpl extends ServiceImpl implements UserThirdService { + + @Override + public UserThird queryByOpenId(ThirdTypeEnum thirdType, String openId) { + return getOne(Wrappers.lambdaQuery() + .eq(UserThird::getThirdType, thirdType.name()) + .eq(UserThird::getOpenId, openId) + .eq(UserThird::getIsDelete, false) + .orderByDesc(UserThird::getId) + .last("limit 1")); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/utils/BeanConver.java b/sonic-bear/server/src/main/java/com/sonic/bear/utils/BeanConver.java new file mode 100644 index 0000000..2669553 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/utils/BeanConver.java @@ -0,0 +1,64 @@ +package com.sonic.bear.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cglib.beans.BeanCopier; + +import java.util.ArrayList; +import java.util.List; + +/** + * 对象拷贝 + * @author dev-center + */ +public class BeanConver { + private final static Logger LOG = LoggerFactory.getLogger(BeanConver.class); + + /** + * 实例类转换 + * + * @param source 源对象 + * @param target 目标对象 + * @param 源对象 + * @param 目标对象 + */ + public static K copeBean(T source, Class target) { + if (source == null) { + return null; + } + BeanCopier beanCopier = BeanCopier.create(source.getClass(), target, false); + K result = null; + try { + result = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(source, result, null); + return result; + } + + /** + * page实例类转换 + * + * @param 源对象 + * @param 目标对象 + * @param source 源对象 + * @param target 目标对象 + * @return + */ + public static List copeList(List source, Class target) { + List list = new ArrayList<>(); + for (T af : source) { + BeanCopier beanCopier = BeanCopier.create(af.getClass(), target, false); + K af1 = null; + try { + af1 = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(af, af1, null); + list.add(af1); + } + return list; + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/utils/HexUtils.java b/sonic-bear/server/src/main/java/com/sonic/bear/utils/HexUtils.java new file mode 100644 index 0000000..1fbfb28 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/utils/HexUtils.java @@ -0,0 +1,69 @@ +package com.sonic.bear.utils; + +public class HexUtils { + private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + public static char[] encodeHex(byte[] data) { + return encodeHex(data, true); + } + + public static char[] encodeHex(byte[] data, boolean toLowerCase) { + return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + public static String encodeHexStr(byte[] data) { + return encodeHexStr(data, true); + } + + public static String encodeHexStr(byte[] data, boolean toLowerCase) { + return encodeHexStr(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + public static byte[] decodeHex(String data) { + return decodeHex(data.toCharArray()); + } + + public static byte[] decodeHex(char[] data) { + int len = data.length; + if ((len & 0x01) != 0) { + throw new RuntimeException("Unknown char"); + } + + byte[] out = new byte[len >> 1]; + for (int i = 0, j = 0; j < len; i++) { + int f = toDigit(data[j], j) << 4; + j++; + f = f | toDigit(data[j], j); + j++; + out[i] = (byte) (f & 0xFF); + } + + return out; + } + + private static char[] encodeHex(byte[] data, char[] toDigits) { + int l = data.length; + char[] out = new char[l << 1]; + for (int i = 0, j = 0; i < l; i++) { + out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; + out[j++] = toDigits[0x0F & data[i]]; + } + return out; + } + + private static String encodeHexStr(byte[] data, char[] toDigits) { + return new String(encodeHex(data, toDigits)); + } + + private static int toDigit(char ch, int index) { + int digit = Character.digit(ch, 16); + if (digit == -1) { + throw new RuntimeException("Invalid hex char " + ch + ", index at " + index); + } + return digit; + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/utils/IdCardGenerator.java b/sonic-bear/server/src/main/java/com/sonic/bear/utils/IdCardGenerator.java new file mode 100644 index 0000000..6317ff7 --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/utils/IdCardGenerator.java @@ -0,0 +1,70 @@ +package com.sonic.bear.utils; + +import com.sonic.bear.enums.ToastResultCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.security.MessageDigest; +import java.time.LocalDate; +import java.time.Year; +import java.util.Random; + +@Slf4j +@Service +public class IdCardGenerator { + + private static final int MAX_STEP = 999; + private static final int MAX_RANDOM = 9999; + + private final Random random = new Random(); + + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + + public String generateIdCard() { + try { + // 获取当前年份和一年中的天数 + LocalDate today = LocalDate.now(); + int yearLastDigit = Year.now().getValue() % 10; + int dayOfYear = today.getDayOfYear(); + + // 生成随机数 (0001-9999) + int randomNum = random.nextInt(MAX_RANDOM) + 1; + + // 使用 Redis 计数器生成唯一值,并通过哈希生成最后三位 + Long counter = stringRedisTemplate.opsForValue().increment(redisKeyUtils.idCardCounterKey()); + if (counter == null) { + stringRedisTemplate.opsForValue().set(redisKeyUtils.idCardCounterKey(), "0"); + counter = stringRedisTemplate.opsForValue().increment(redisKeyUtils.idCardCounterKey()); + } + if (counter > MAX_STEP) { + stringRedisTemplate.opsForValue().set(redisKeyUtils.idCardCounterKey(), "0"); + counter = 1L; + } + + // 使用计数器和时间戳生成哈希值,确保步长非连续 + String hashInput = counter + String.valueOf(System.currentTimeMillis()); + int step = (Math.abs(hashString(hashInput)) % MAX_STEP) + 1; // 001-999 + + // 格式化 ID:年(1位) + 日期(3位) + 随机数(4位) + 步长(3位) + return String.format("%1d%03d%04d%03d", yearLastDigit, dayOfYear, randomNum, step); + } catch (Exception e) { + log.error("===> generateIdCard error : ", e); + ToastResultCode.BIZ_ERROR1.check(true); + } + return null; + } + + private int hashString(String input) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes()); + // 使用前4字节生成整数 + return ((hash[0] & 0xFF) << 24) | ((hash[1] & 0xFF) << 16) | + ((hash[2] & 0xFF) << 8) | (hash[3] & 0xFF); + } +} \ No newline at end of file diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/utils/PasswordUtils.java b/sonic-bear/server/src/main/java/com/sonic/bear/utils/PasswordUtils.java new file mode 100644 index 0000000..f7e5d2f --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/utils/PasswordUtils.java @@ -0,0 +1,54 @@ +package com.sonic.bear.utils; + +import java.security.MessageDigest; +import java.util.Random; + +public class PasswordUtils { + + public static String generate(String password) { + Random r = new Random(); + StringBuilder sb = new StringBuilder(16); + sb.append(r.nextInt(99999999)).append(r.nextInt(99999999)); + int len = sb.length(); + if (len < 16) { + for (int i = 0; i < 16 - len; i++) { + sb.append("0"); + } + } + String salt = sb.toString(); + password = md5Hex(password + salt); + char[] cs = new char[48]; + for (int i = 0; i < 48; i += 3) { + cs[i] = password.charAt(i / 3 * 2); + char c = salt.charAt(i / 3); + cs[i + 1] = c; + cs[i + 2] = password.charAt(i / 3 * 2 + 1); + } + return new String(cs); + } + + /** + * 获取十六进制字符串形式的MD5摘要 + */ + private static String md5Hex(String src) { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] bs = md5.digest(src.getBytes()); + return HexUtils.encodeHexStr(bs); + } catch (Exception e) { + return null; + } + } + + public static boolean verify(String password, String md5) { + char[] cs1 = new char[32]; + char[] cs2 = new char[16]; + for (int i = 0; i < 48; i += 3) { + cs1[i / 3 * 2] = md5.charAt(i); + cs1[i / 3 * 2 + 1] = md5.charAt(i + 2); + cs2[i / 3] = md5.charAt(i + 1); + } + String salt = new String(cs2); + return md5Hex(password + salt).equals(new String(cs1)); + } +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/utils/RedisKeyUtils.java b/sonic-bear/server/src/main/java/com/sonic/bear/utils/RedisKeyUtils.java new file mode 100644 index 0000000..be3745d --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/utils/RedisKeyUtils.java @@ -0,0 +1,106 @@ +package com.sonic.bear.utils; + +import com.sonic.common.AppRuntime; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * 统一的 redis key 管理工具类(方便后续的维护和查找) + * + * @Author code + * @Date 2021/9/24 + * @Version 1.0 + */ +@Slf4j +@Service +public class RedisKeyUtils { + + @Value("${spring.profiles.active}") + private String runMode; + @Autowired + private AppRuntime appRuntime; + + ///////////////////////////////////////// 综合推荐业务降级 ///////////////////////////////////////// + + /** + * 超时降级次数校验RedisKey + * + * @param bizType + * @return + */ + public String fallBackTimeOutCountCheckKey(String bizType) { + if(runMode.equals("test")) { + return "1001:test:fallback:timeoutCount:" + bizType; + } + return "1001:fallback:timeoutCount:" + bizType; + } + + /** + * 超时降级的状态 + * @param bizType + * @return + */ + public String fallBackKey(String bizType) { + if(runMode.equals("test")) { + return "1001:test:fallback:status:" + bizType; + } + return "1001:fallback:status:" + bizType; + } + + /** + * 创建session时的redis锁(eg: 1001:test:lock:createSession:xxx) + * @param userId + * @return + */ + public String sessionCreateLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "createSession", userId); + } + + /** + * 创建session时的redis锁(eg: 1001:test:lock:createSession:xxx) + * @param userId + * @return + */ + public String addAccessLogLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "addAccessLog", userId); + } + + /** + * 创建session时的redis锁(eg: 1001:test:session:kickOut:xxx) + * @param token + * @return + */ + public String sessionKickOutKey(String token) { + return appRuntime.buildPrefixKey("session", "kickOut", token); + } + + /** + * 创建session时的redis锁(eg: 1001:test:session:access:xxx) + * @param userId + * @return + */ + public String addAccessLogKey(Long userId) { + return appRuntime.buildPrefixKey("session", "access", userId); + } + + + /** + * 用户ID的列表(eg: 1001:test:idCard:genList) + * @return + */ + public String idCardGenListKey() { + return appRuntime.buildPrefixKey("idCard", "genList"); + } + + /** + * 用户ID的最大值(eg: 1001:test:idCard:maxValue) + * @return + */ + public String idCardCounterKey() { + return appRuntime.buildPrefixKey("idCard", "counter"); + } + + +} diff --git a/sonic-bear/server/src/main/java/com/sonic/bear/utils/UUIDGenerator.java b/sonic-bear/server/src/main/java/com/sonic/bear/utils/UUIDGenerator.java new file mode 100644 index 0000000..6767f5e --- /dev/null +++ b/sonic-bear/server/src/main/java/com/sonic/bear/utils/UUIDGenerator.java @@ -0,0 +1,100 @@ +package com.sonic.bear.utils; + +import java.util.Random; +import java.util.UUID; + +public class UUIDGenerator { + + private static final String SPLITOR = "-"; + private static final String BLANK = ""; + + public static String BASE_STRING = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + public static String[] BASE_CHARS = new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", + "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", + "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", + "T", "U", "V", "W", "X", "Y", "Z"}; + + /** UUID 32位 */ + public static String generateLongUuid() { + return generateLongUuid(true); + } + + /** UUID 32位 */ + public static String generateLongUuid(boolean isUpperCase) { + String uuid = UUID.randomUUID().toString().replace(SPLITOR, BLANK); + if (isUpperCase) { + return uuid.toUpperCase(); + } + return uuid.toLowerCase(); + } + + /** UUID转为8位 */ + public static String generateShortUuid() { + StringBuilder shortBuffer = new StringBuilder(8); + String uuid = UUID.randomUUID().toString().replace(SPLITOR, BLANK); + for (int i = 0; i < 8; i++) { + String str = uuid.substring(i * 4, i * 4 + 4); + int x = Integer.parseInt(str, 16); + shortBuffer.append(BASE_CHARS[x % 0x3E]); + } + return shortBuffer.toString(); + } + + public static String generate16Uuid() { + StringBuilder shortBuffer = new StringBuilder(16); + String uuid = UUID.randomUUID().toString().replace(SPLITOR, BLANK); + for (int i = 0; i < 16; i++) { + int start = i * 2; + int end = Math.min(i * 2 + 4, 32); + String str = uuid.substring(start, end); + int x = Integer.parseInt(str, 16); + shortBuffer.append(BASE_CHARS[x % 0x3E]); + } + return shortBuffer.toString(); + } + + /** + * 生成32位的token + * @return + */ + public static String generate32Uuid() { + return generate16Uuid() + generate16Uuid(); + } + + /** + * 生成48位的token + * @return + */ + public static String generate48Uuid() { + return generate16Uuid() + generate16Uuid() + generate16Uuid(); + } + + /** + * 生成64位的token + * @return + */ + public static String generate64Uuid() { + return generate16Uuid() + generate16Uuid() + generate16Uuid() + generate16Uuid(); + } + + /** length表示生成字符串的长度 */ + public static String getRandomString(int length) { + Random random = new Random(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + int number = random.nextInt(BASE_STRING.length()); + sb.append(BASE_STRING.charAt(number)); + } + return sb.toString(); + } + + public static void main(String[] args) { + long start = System.currentTimeMillis(); + for (int i = 0; i < 30000; i++) { + System.out.println("D" + generateShortUuid() + getRandomString(0)); + } + System.out.println(System.currentTimeMillis() - start); + } + +} \ No newline at end of file diff --git a/sonic-bear/server/src/main/resources/application-dev.yml b/sonic-bear/server/src/main/resources/application-dev.yml new file mode 100644 index 0000000..a25f362 --- /dev/null +++ b/sonic-bear/server/src/main/resources/application-dev.yml @@ -0,0 +1,35 @@ +spring: + datasource: + # TODO: 需要改写为Dev环境的Mysql地址, 数据库名称,用户名和密码 + url: jdbc:mysql://54.223.196.180:3306/sonic-bear?autoReconnect=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull + username: root + password: 123456 + + redis: + # TODO: 需要改写为DEV环境的Redis地址, 数据库名称和密码 + host: 54.223.196.180 + port: 6379 + database: 0 + password: 123456 + # cluster: + # nodes: 192.168.100.238 + # ssl: + # enabled: true + + rabbitmq: + # TODO: 需要改写为dev环境的rabbitmq地址, 用户名和密码 + host: 54.223.196.180 + port: 5672 + username: guest + password: toukagames1234 + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bear.controller" \ No newline at end of file diff --git a/sonic-bear/server/src/main/resources/application-local.yml b/sonic-bear/server/src/main/resources/application-local.yml new file mode 100644 index 0000000..67fc857 --- /dev/null +++ b/sonic-bear/server/src/main/resources/application-local.yml @@ -0,0 +1,35 @@ +spring: + datasource: + # TODO: 需要改写为Dev环境的Mysql地址, 数据库名称,用户名和密码 + url: jdbc:mysql://192.168.100.238:3306/sonic-bear?autoReconnect=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull + username: egirl_dev + password: lpkq609oI9eRc + + redis: + # TODO: 需要改写为DEV环境的Redis地址, 数据库名称和密码 + host: 192.168.100.238 + port: 6379 + database: 0 + password: Epal@2020 +# cluster: +# nodes: 192.168.100.238 +# ssl: +# enabled: true + + rabbitmq: + # TODO: 需要改写为dev环境的rabbitmq地址, 用户名和密码 + host: 192.168.100.238 + port: 5672 + username: guest + password: epal@2020 + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bear.controller" \ No newline at end of file diff --git a/sonic-bear/server/src/main/resources/application-product.yml b/sonic-bear/server/src/main/resources/application-product.yml new file mode 100644 index 0000000..aa0098f --- /dev/null +++ b/sonic-bear/server/src/main/resources/application-product.yml @@ -0,0 +1,34 @@ +spring: + datasource: + url: ${DB.MASTER.BEAR.URL} + username: ${DB.MASTER.USERNAME} + password: ${DB.MASTER.PASSWORD} + + redis: +# host: ${REDIS.MAIN.HOST} +# port: ${REDIS.MAIN.PORT} +# database: 0 +# password: ${REDIS.MAIN.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + + rabbitmq: + host: ${MQ.HOST} + port: ${MQ.PORT} + username: ${MQ.USERNAME} + password: ${MQ.PASSWORD} + ssl: + enabled: true + +web: + frequency-alert: + # TODO 修改接口访问频率告警配置。 + # 默认配置含义为: /* 所有接口5秒访问500次. + rules: + - /*:5/500 + +#swagger展示相关的配置 +swagger: + enabled: false + base: + package: "com.sonic.bear.controller" diff --git a/sonic-bear/server/src/main/resources/application-test.yml b/sonic-bear/server/src/main/resources/application-test.yml new file mode 100644 index 0000000..63adf7f --- /dev/null +++ b/sonic-bear/server/src/main/resources/application-test.yml @@ -0,0 +1,32 @@ +spring: + datasource: + url: ${DB.MASTER.BEAR.URL} + username: ${DB.MASTER.USERNAME} + password: ${DB.MASTER.PASSWORD} + + redis: +# host: ${REDIS.MAIN.HOST} +# port: ${REDIS.MAIN.PORT} +# database: 0 +# password: ${REDIS.MAIN.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + + rabbitmq: + host: ${MQ.HOST} + port: ${MQ.PORT} + username: ${MQ.USERNAME} + password: ${MQ.PASSWORD} + ssl: + enabled: true + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bear.controller" \ No newline at end of file diff --git a/sonic-bear/server/src/main/resources/application.yml b/sonic-bear/server/src/main/resources/application.yml new file mode 100644 index 0000000..1fb5ae0 --- /dev/null +++ b/sonic-bear/server/src/main/resources/application.yml @@ -0,0 +1,79 @@ +spring: + profiles: + # profile目前支持以下5种:local/unittest/dev/test/product + # 开发的时候一般使用dev或者local + # 在测试环境/生产环境,该配置不起作用,会被外部传入的jvm启动参数(spring.profiles.active)或者环境变量覆盖 + active: local + application: + # TODO: 更换项目名称 + name: bear + # 必须使用引号,否则会转成8进制 + id: 1000 + task: + execution: + pool: + max-size: 50 + core-size: 4 + queue-capacity: 20480 + keep-alive: 30s + # TODO: 如果不需要mysql,请移除datasource相关的所有配置 + datasource: + driver-class-name: com.mysql.jdbc.Driver + hikari: + auto-commit: true + connection-timeout: 20000 + maximum-pool-size: 30 + minimum-idle: 5 + idle-timeout: 300000 + max-lifetime: 1200000 + # TODO: 如果不需要Redis,请移除redis相关的所有配置 + redis: + lettuce: + pool: + max-active: 1000 + max-wait: 1000 + max-idle: 100 + + rabbitmq: + listener: + simple: + acknowledge-mode: manual + concurrency: 2 + max-concurrency: 10 + #限流 + prefetch: 1 + +# TODO: 如果不需要mysql,请移除mybatis-plus相关的所有配置 +mybatis-plus: + # 定义mybatis映射文件的位置 + mapper-locations: classpath:/mapper/*Mapper.xml + +web: + frequency-alert: + # TODO 修改接口访问频率告警配置, 如果不配置的FrequencyAlertInterceptor不会生效. + # 以下配置含义为: /* 1秒访问20次, /index 10秒访问100次. 访问频率超过该规则会触发告警consumer + rules: + - /*:1/20 + - /index:10/100 + +mq: + # 如无必要,无须修改 exchange + exchange: message-server-exchange + default: + # TODO: {Event.BuildInScene.code}-{appName}-queue + queue: bs-bear-queue + # TODO: {Event.BuildInScene.code}-{appName}-routing-key + routing-key: bs-bear-routing-key + #用户注册后处理 + user-created: + queue: user-created-queue + routing-key: user-created-routing-key + +# swagger 默认开启,在生产环境关闭,节省资源 +swagger: + enabled: true + +# 禁用健康检查 +management: + endpoints: + enabled-by-default: false #关闭监控 diff --git a/sonic-bear/server/src/main/resources/logback-spring.xml b/sonic-bear/server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b8dd4d1 --- /dev/null +++ b/sonic-bear/server/src/main/resources/logback-spring.xml @@ -0,0 +1,62 @@ + + + + + + + + + + ${DefaultPattern} + + + + + + + + ${ColorfulPattern} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sonic-bear/server/src/main/resources/messages.properties b/sonic-bear/server/src/main/resources/messages.properties new file mode 100644 index 0000000..03305f4 --- /dev/null +++ b/sonic-bear/server/src/main/resources/messages.properties @@ -0,0 +1,10 @@ +NO_LOGIN=User is not logged in +USER_NOT_EXIST=User does not exist +APP_CLIENT_NOT_EXIST=Login client does not exist +APP_CLIENT_NOT_ALLOW=Not authorized to log in to the client +SESSION_EXPIRED=Session is expired +SESSION_INVALID=Session is invalid +PASSWORD_INVALID=Invalid account or password +AUTH_FAIL=Authorization failed +USER_FROZEN=User is frozen +BIZ_ERROR1=Biz error \ No newline at end of file diff --git a/sonic-bear/server/src/main/resources/messages_en.properties b/sonic-bear/server/src/main/resources/messages_en.properties new file mode 100644 index 0000000..03305f4 --- /dev/null +++ b/sonic-bear/server/src/main/resources/messages_en.properties @@ -0,0 +1,10 @@ +NO_LOGIN=User is not logged in +USER_NOT_EXIST=User does not exist +APP_CLIENT_NOT_EXIST=Login client does not exist +APP_CLIENT_NOT_ALLOW=Not authorized to log in to the client +SESSION_EXPIRED=Session is expired +SESSION_INVALID=Session is invalid +PASSWORD_INVALID=Invalid account or password +AUTH_FAIL=Authorization failed +USER_FROZEN=User is frozen +BIZ_ERROR1=Biz error \ No newline at end of file diff --git a/sonic-common-api/.gitignore b/sonic-common-api/.gitignore new file mode 100644 index 0000000..6085469 --- /dev/null +++ b/sonic-common-api/.gitignore @@ -0,0 +1,27 @@ +/**/rebel.xml + +#target/ +target/ + +# IDEA # +.idea/ +*.iml + +# Eclipse # +.settings/ +.classpath +.project + +# Log # +logs/ +*.log% + +#mac +.DS_Store + +#svn +.svn + +#reviewboardrc +.reviewboardrc + diff --git a/sonic-common-api/README.md b/sonic-common-api/README.md new file mode 100644 index 0000000..989a800 --- /dev/null +++ b/sonic-common-api/README.md @@ -0,0 +1,11 @@ +# common-api + 通用api层(web层) sdk包 +# 作用概述 + common-api 主要用于处理 web层通用逻辑 . + 1.通用的filter工具类 例如获取requestBody数据 解密. + 2.通用的 请求相应的 bean + 3.通用的 参数包装处理 例如授权 用户信息等 + 4.统一的 返回相应异常拦截 + 5.自动装配 + 6.其他与 web层 通用逻辑相关代码 + \ No newline at end of file diff --git a/sonic-common-api/pom.xml b/sonic-common-api/pom.xml new file mode 100644 index 0000000..654ba97 --- /dev/null +++ b/sonic-common-api/pom.xml @@ -0,0 +1,134 @@ + + + 4.0.0 + + com.sonic.sdk + sonic-common-api + 1.0.1-SNAPSHOT + + + + org.springframework.cloud + spring-cloud-starter-openfeign + 2.0.0.RELEASE + + + com.google.code.findbugs + jsr305 + + + org.hdrhistogram + HdrHistogram + + + provided + + + org.bouncycastle + bcprov-jdk16 + 1.46 + + + com.alibaba + fastjson + 1.2.62 + + + org.apache.commons + commons-lang3 + 3.0 + + + org.springframework.boot + spring-boot-autoconfigure + + + org.projectlombok + lombok + 1.18.40 + + + org.springframework + spring-core + 5.0.8.RELEASE + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-redis + + + logback-classic + ch.qos.logback + + + logback-core + ch.qos.logback + + + + + + + + + + org.springframework.boot + spring-boot-dependencies + 1.5.2.RELEASE + pom + import + + + + + + + + + + + + + + + + + + + + + + + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/EnableDecrypt.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/EnableDecrypt.java new file mode 100644 index 0000000..2127bcc --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/EnableDecrypt.java @@ -0,0 +1,16 @@ +package com.sonic.sdk.api.annotation; + +import com.sonic.sdk.api.autoconfigure.DecryptApiConfig; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * 是否开启 加解密功能 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import({DecryptApiConfig.class}) +public @interface EnableDecrypt { +} \ No newline at end of file diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/EnableGatWayAuthScan.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/EnableGatWayAuthScan.java new file mode 100644 index 0000000..99cbc42 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/EnableGatWayAuthScan.java @@ -0,0 +1,17 @@ +package com.sonic.sdk.api.annotation; + +import com.sonic.sdk.api.autoconfigure.IgnoreAuthRegistConfig; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * 是否开启 扫描 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import({IgnoreAuthRegistConfig.class}) +public @interface EnableGatWayAuthScan { + String basePackages() default "com"; +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/IgnoreAuth.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/IgnoreAuth.java new file mode 100644 index 0000000..2047816 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/IgnoreAuth.java @@ -0,0 +1,13 @@ +package com.sonic.sdk.api.annotation; + +import java.lang.annotation.*; + +/** + * 用于标记 不需要授权的 URL 方法 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface IgnoreAuth { +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/InternalRpc.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/InternalRpc.java new file mode 100644 index 0000000..ba1cc15 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/annotation/InternalRpc.java @@ -0,0 +1,18 @@ +package com.sonic.sdk.api.annotation; + +import java.lang.annotation.*; + +/** + * 用于标记 内部调用。 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface InternalRpc { + /** + * 支持 正则表达式的 路径 + * @return + */ + String path(); +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/autoconfigure/DecryptApiConfig.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/autoconfigure/DecryptApiConfig.java new file mode 100644 index 0000000..08bafa1 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/autoconfigure/DecryptApiConfig.java @@ -0,0 +1,15 @@ +package com.sonic.sdk.api.autoconfigure; + + +import com.sonic.sdk.api.filters.DecryptRequestBodyFilter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; + + +@Slf4j +public class DecryptApiConfig { + @Bean + public DecryptRequestBodyFilter decryptRequestBodyFilter() { + return new DecryptRequestBodyFilter(); + } +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/autoconfigure/FeignConfiguration.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/autoconfigure/FeignConfiguration.java new file mode 100644 index 0000000..bcaa0a9 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/autoconfigure/FeignConfiguration.java @@ -0,0 +1,14 @@ +package com.sonic.sdk.api.autoconfigure; + +import com.sonic.sdk.api.interceptors.FeignTraceInterceptor; + +//@Configuration +public class FeignConfiguration { + +// @Bean + public FeignTraceInterceptor traceInterceptor() { + return new FeignTraceInterceptor(); + } + +} + diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/autoconfigure/IgnoreAuthRegistConfig.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/autoconfigure/IgnoreAuthRegistConfig.java new file mode 100644 index 0000000..489be29 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/autoconfigure/IgnoreAuthRegistConfig.java @@ -0,0 +1,35 @@ +package com.sonic.sdk.api.autoconfigure; + +import com.sonic.sdk.api.annotation.EnableGatWayAuthScan; +import com.sonic.sdk.api.utils.GateWayAuthScanRunner; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.type.AnnotationMetadata; + +import java.util.Map; + +@Slf4j +public class IgnoreAuthRegistConfig implements ImportAware { + + private String baseBackages; + + @Bean + GateWayAuthScanRunner autoScanIgnoreAuth() { + return new GateWayAuthScanRunner(baseBackages); + } + + + protected String getBasePackages(AnnotationMetadata importingClassMetadata) { + Map attributes = importingClassMetadata + .getAnnotationAttributes(EnableGatWayAuthScan.class.getCanonicalName()); + String pkg = (String) attributes.get("basePackages"); + return pkg; + } + + + @Override + public void setImportMetadata(AnnotationMetadata annotationMetadata) { + this.baseBackages = getBasePackages(annotationMetadata); + } +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/constants/ApiConstant.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/constants/ApiConstant.java new file mode 100644 index 0000000..2492ec6 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/constants/ApiConstant.java @@ -0,0 +1,5 @@ +package com.sonic.sdk.api.constants; + +public interface ApiConstant { + String LOG_TRACE_ID = "traceId"; +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/constants/WebHeaderConstant.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/constants/WebHeaderConstant.java new file mode 100644 index 0000000..c8f5b05 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/constants/WebHeaderConstant.java @@ -0,0 +1,6 @@ +package com.sonic.sdk.api.constants; + +public interface WebHeaderConstant { + String AUTH_TOKEN = "AUTH_TK"; + String ENCRYPT = "encrypt"; +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/exceptions/ApiRequestTimeOutException.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/exceptions/ApiRequestTimeOutException.java new file mode 100644 index 0000000..efefc60 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/exceptions/ApiRequestTimeOutException.java @@ -0,0 +1,7 @@ +package com.sonic.sdk.api.exceptions; + +public class ApiRequestTimeOutException extends RuntimeException { + public ApiRequestTimeOutException(String message) { + super(message); + } +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/exceptions/DecryptFailException.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/exceptions/DecryptFailException.java new file mode 100644 index 0000000..c267065 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/exceptions/DecryptFailException.java @@ -0,0 +1,8 @@ +package com.sonic.sdk.api.exceptions; + +public class DecryptFailException extends RuntimeException { + public DecryptFailException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/filters/DecryptRequestBodyFilter.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/filters/DecryptRequestBodyFilter.java new file mode 100644 index 0000000..9d51621 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/filters/DecryptRequestBodyFilter.java @@ -0,0 +1,74 @@ +package com.sonic.sdk.api.filters; + +import com.alibaba.fastjson.JSON; +import com.sonic.sdk.api.exceptions.ApiRequestTimeOutException; +import com.sonic.sdk.api.exceptions.DecryptFailException; +import com.sonic.sdk.api.utils.http.DecryptRequestBodyWrapper; +import com.sonic.sdk.api.vo.Result; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * requestBody 解密 filter + * 用于通用解密 + */ +public class DecryptRequestBodyFilter implements Filter { + + @Value("${spring.profiles.active:prod}") + private String profile; + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + ServletRequest requestWrapper = null; + //排除dev local 环境. + boolean local = "local".equals(profile) ; + if (request instanceof HttpServletRequest && !local) { + try { + requestWrapper = new DecryptRequestBodyWrapper((HttpServletRequest) request); + } catch (DecryptFailException e) { + Result r = new Result(); + r.setContent(null); + r.setStatus("ERROR"); + r.setErrorCode("ERROR"); + r.setErrorMsg("Request parameter decryption failed"); + String resp = JSON.toJSONString(r); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("Utf-8"); + response.getWriter().write(resp); + response.getWriter().flush(); + return; + } catch (ApiRequestTimeOutException e) { + Result r = new Result(); + r.setContent(null); + r.setStatus("ERROR"); + r.setErrorCode("ERROR"); + r.setErrorMsg("Request timeout"); + String resp = JSON.toJSONString(r); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("Utf-8"); + response.getWriter().write(resp); + response.getWriter().flush(); + return; + } + } + if (requestWrapper == null) { + chain.doFilter(request, response); + } else { + chain.doFilter(requestWrapper, response); + } + } + + + @Override + public void init(FilterConfig filterConfig) { + } +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/interceptors/FeignTraceInterceptor.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/interceptors/FeignTraceInterceptor.java new file mode 100644 index 0000000..2583e63 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/interceptors/FeignTraceInterceptor.java @@ -0,0 +1,22 @@ +package com.sonic.sdk.api.interceptors; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.MDC; + +/** + * Feign请求拦截器 + * + * @author yinjihuan + * @create 2017-11-10 17:25 + **/ +public class FeignTraceInterceptor implements RequestInterceptor { + @Override + public void apply(RequestTemplate requestTemplate) { + String traceId = MDC.get("TRACE_ID"); + if(StringUtils.isNotEmpty(traceId)){ + requestTemplate.header("X-TRACE-ID", MDC.get("TRACE_ID")); + } + } +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/AesEncodeUtil.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/AesEncodeUtil.java new file mode 100644 index 0000000..a8fd7c7 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/AesEncodeUtil.java @@ -0,0 +1,60 @@ +package com.sonic.sdk.api.utils; + +import org.apache.tomcat.util.codec.binary.Base64; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.validation.constraints.NotNull; +import java.security.Security; +import java.security.spec.AlgorithmParameterSpec; + +public final class AesEncodeUtil { + + //偏移量 + public static final String VIPARA = "dFd8s4fDfV6d2fsD"; + + //编码方式 + private static final String CHARSET_NAME = "UTF-8"; + + private static final String AES_NAME = "AES"; + //填充类型 + public static final String ALGORITHM = "AES/CBC/PKCS7Padding"; + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * 加密 + * + * @param content + * @param key + * @return + */ + public static String encrypt(@NotNull String content, @NotNull String key) throws Exception { + byte[] result = null; + Cipher cipher = Cipher.getInstance(ALGORITHM); + SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(CHARSET_NAME), AES_NAME); + AlgorithmParameterSpec paramSpec = new IvParameterSpec(VIPARA.getBytes()); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, paramSpec); + result = cipher.doFinal(content.getBytes(CHARSET_NAME)); + return Base64.encodeBase64String(result); + } + + /** + * 解密 + * + * @param content + * @param key + * @return + */ + public static String decrypt(@NotNull String content, @NotNull String key) throws Exception { + Cipher cipher = Cipher.getInstance(ALGORITHM); + SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(CHARSET_NAME), AES_NAME); + AlgorithmParameterSpec paramSpec = new IvParameterSpec(VIPARA.getBytes()); + cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec); + return new String(cipher.doFinal(Base64.decodeBase64(content)), CHARSET_NAME); + } +} \ No newline at end of file diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/GateWayAuthScanRunner.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/GateWayAuthScanRunner.java new file mode 100644 index 0000000..4b5e87c --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/GateWayAuthScanRunner.java @@ -0,0 +1,146 @@ +package com.sonic.sdk.api.utils; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.ImmutableMap; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import com.sonic.sdk.api.annotation.InternalRpc; +import com.sonic.sdk.api.vo.ServiceAuthRegist; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Slf4j +public class GateWayAuthScanRunner implements ApplicationRunner { + + @Autowired + private RestTemplate restTemplate; + + @Value("${spring.application.name}") + private String applicationName; + + private String baseBackages; + + private static final String SECRET_KEY = "l5lUstfWcb3GlWqiiHJKVLKTSvwJSqrT"; + + public GateWayAuthScanRunner(String baseBackages) { + this.baseBackages = baseBackages; + } + + @IgnoreAuth + @Override + public void run(ApplicationArguments applicationArguments) throws Exception { + // true:默认TypeFilter生效,这种模式会查询出许多不符合你要求的class名 + // false:关闭默认TypeFilter + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AnnotationTypeFilter(RestController.class)); + Set beanDefinitionSet = provider.findCandidateComponents(this.baseBackages); + List ignoreAuthPathList = new ArrayList(); + List internalUrlList = new ArrayList(); + List allUrlList = new ArrayList(); + log.info("开始扫描 非授权URL 和 黑名单 包名: {} .", baseBackages); + for (BeanDefinition beanDefinition : beanDefinitionSet) { + String classPth = ""; + Class clazz = Class.forName(beanDefinition.getBeanClassName()); + RequestMapping requestMapping = (RequestMapping) clazz.getAnnotation(RequestMapping.class); + InternalRpc internalRpc = (InternalRpc) clazz.getAnnotation(InternalRpc.class); + if (internalRpc != null) { + internalUrlList.add(internalRpc.path()); + } + + if (requestMapping != null) { + classPth = requestMapping.value()[0]; + } + Method methods[] = clazz.getDeclaredMethods(); + for (Method method : methods) { + String url = classPth + getMethodPath(method); + //将非/api/和非/admin/开头的url接口加入到所有接口列表中 + if(StringUtils.isNotEmpty(url) && !url.equals("null") && + !url.startsWith("/api/") && !url.startsWith("api/") && !url.startsWith("/admin/") && !url.startsWith("admin/")) { + allUrlList.add(url); + } + IgnoreAuth ignoreAuthMethod = method.getAnnotation(IgnoreAuth.class); + if (ignoreAuthMethod == null) { + continue; + } + //忽略登录认证的接口,需要排除掉 /api/和/admin的数据这样减少一点内存的占用 + if(StringUtils.isNotEmpty(url) && !url.equals("null") && + !url.startsWith("/api/") && !url.startsWith("api/") && !url.startsWith("/admin/") && !url.startsWith("admin/")) { + ignoreAuthPathList.add(url); + } + } + } + + String env = SpringContextUtil.getActiveProfile(); + Map hostMaps = ImmutableMap.of("dev", "http://localhost:8082", + "local", "http://localhost:8082", + "test", "http://test-gateway-svc:8080", + "staging", "http://prod-gateway-svc:8080", + "product", "http://prod-gateway-svc:8080" + ); + String url = hostMaps.get(env) + "/api/gateway/auth/register"; + log.info("上报信息 url:{}", url); + try { + ServiceAuthRegist serviceAuthRegist = new ServiceAuthRegist(); + serviceAuthRegist.setApplicationName(this.applicationName); + serviceAuthRegist.setIgnoreAuthList(ignoreAuthPathList); + serviceAuthRegist.setUrlBlacklist(internalUrlList); + serviceAuthRegist.setUrlAllList(allUrlList); + String json = JSON.toJSONString(serviceAuthRegist); + String content = AesEncodeUtil.encrypt(json, SECRET_KEY); + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.setContentType(MediaType.APPLICATION_JSON); + Map map = ImmutableMap.of("content", content); + HttpEntity requestEntity = new HttpEntity(JSONObject.toJSONString(map), requestHeaders); + String result = restTemplate.postForObject(url, requestEntity, String.class); + log.info("===> 上报网关服务 结果: {}", result); + } catch (Exception e) { + log.error("上报网关服务 授权信息 异常", e); + } + log.info("上报网关服务 授权信息 结束"); + } + + private String getMethodPath(Method method) { + PostMapping postMappingMethod = method.getAnnotation(PostMapping.class); + if (postMappingMethod != null) { + return postMappingMethod.value()[0]; + } + GetMapping getMappingMethod = method.getAnnotation(GetMapping.class); + if (getMappingMethod != null) { + return getMappingMethod.value()[0]; + } + DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class); + if (deleteMapping != null) { + return deleteMapping.value()[0]; + } + + PutMapping putMapping = method.getAnnotation(PutMapping.class); + if (putMapping != null) { + return putMapping.value()[0]; + } + + RequestMapping requestMappingMethod = method.getAnnotation(RequestMapping.class); + if (requestMappingMethod != null) { + return requestMappingMethod.value()[0]; + } + return null; + } + +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/MD5Utils.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/MD5Utils.java new file mode 100644 index 0000000..f9a18c9 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/MD5Utils.java @@ -0,0 +1,51 @@ +package com.sonic.sdk.api.utils; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class MD5Utils { + + + public static String stringToMD5(String plainText) { + byte[] secretBytes = null; + try { + secretBytes = MessageDigest.getInstance("md5").digest( + plainText.getBytes()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("没有这个md5算法!"); + } + String md5code = new BigInteger(1, secretBytes).toString(16); + final int length = md5code.length(); + for (int i = 0; i < 32 - length; i++) { + md5code = "0" + md5code; + } + return md5code; + } + + + public static void main(String[] args) throws Exception { + + String token = "WRTRthzsN07fq1i2"; + String mix = "90e8kDQUIWpXg8Jp"; + String content = "{\n" + + " \"productId\":164\n" + + "}"; + String key = stringToMD5("Pxtw10SD").toUpperCase(); + System.out.println("key: "+key.getBytes().length*8); + System.out.println(key); + + System.out.println("加密后:"+ AesEncodeUtil.encrypt(content,key)); + String encoded = + + "4yRTJ9vP+DTRhZ4vq8EIPLTlEEgFj2qmGj0bwamDpnWRXCyNiaVpJP64EnowPMURgf3L15m5IGzupXKQev8J1fMYj4maoVZEJcEkEOplOLA="; + // key ="F77D3FBE46C0333ED5B6D5A84B99EAE4"; + String decode = AesEncodeUtil.decrypt( encoded,key); + System.out.println("解密后"+decode); + } + + + + + +} \ No newline at end of file diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/ReponseUtil.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/ReponseUtil.java new file mode 100644 index 0000000..a5ca98e --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/ReponseUtil.java @@ -0,0 +1,5 @@ +package com.sonic.sdk.api.utils; + +public class ReponseUtil { + +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/SpringContextUtil.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/SpringContextUtil.java new file mode 100644 index 0000000..912d033 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/SpringContextUtil.java @@ -0,0 +1,43 @@ +package com.sonic.sdk.api.utils; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import java.util.Locale; + +@Configuration +public class SpringContextUtil implements ApplicationContextAware { + + private static ApplicationContext context = null; + + /* (non Javadoc) + * @Title: setApplicationContext + * @Description: spring获取bean工具类 + * @param applicationContext + * @throws BeansException + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.context = applicationContext; + } + + // 传入线程中 + public static T getBean(String beanName) { + return (T) context.getBean(beanName); + } + + // 国际化使用 + public static String getMessage(String key) { + return context.getMessage(key, null, Locale.getDefault()); + } + + /// 获取当前环境 + public static String getActiveProfile() { + return context.getEnvironment().getActiveProfiles()[0]; + } +} \ No newline at end of file diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/http/DecryptRequestBodyWrapper.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/http/DecryptRequestBodyWrapper.java new file mode 100644 index 0000000..ebecee2 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/http/DecryptRequestBodyWrapper.java @@ -0,0 +1,94 @@ +package com.sonic.sdk.api.utils.http; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.sonic.sdk.api.constants.WebHeaderConstant; +import com.sonic.sdk.api.exceptions.DecryptFailException; +import com.sonic.sdk.api.utils.AesEncodeUtil; +import com.sonic.sdk.api.utils.MD5Utils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +@Slf4j +public class DecryptRequestBodyWrapper extends RequestBodyWrapper { + + /** + * 混淆的 固定字符串 + */ + private static final String MIX = "Q8pD09kIeWXg8Ju"; + + /** + * token密钥的最大长度为 32 位 + */ + private static final Integer TOKEN_KEY_MAX_LEN = 32; + + /** + * 内网访问认证标志密钥 + */ + private static final String IN_AH_CD_VALUE = "JUHYyte656VGT567uygu#2@89JH7g*yg@jxTQPXZMalje3"; + + public DecryptRequestBodyWrapper(HttpServletRequest request) throws IOException { + super(request); + //判断header头中是否携带了内网请求标志,如果有请求标志则不进行解密操作 + String innerAuthCode = request.getHeader("IN_AH_CD"); + if (StringUtils.isEmpty(innerAuthCode)) { + //header头中没有找到的话从cookie中找 + innerAuthCode = getCookieValue(request, "IN_AH_CD"); + } + String token = request.getHeader(WebHeaderConstant.AUTH_TOKEN); + if (StringUtils.isNotEmpty(token) && !IN_AH_CD_VALUE.equals(innerAuthCode) && HttpMethod.POST.name().equals(request.getMethod()) + && (MediaType.APPLICATION_JSON_VALUE.equals(request.getContentType()) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(request.getContentType()))) { + + String oldBodyString = new String(super.body, "UTF-8"); + log.debug("开始解密请求参数:{}", oldBodyString); + //截取token作为解密的密钥 + String tokenKey = token; + if (token.length() > TOKEN_KEY_MAX_LEN) { + tokenKey = token.substring(0, TOKEN_KEY_MAX_LEN); + } + String key = MD5Utils.stringToMD5(tokenKey + MIX).toUpperCase(); + log.debug("开始解密请求参数heder: token : {}, tokenKey: {}, key : {},mix:{}", token, tokenKey, key, MIX); + + JSONObject jsonObject = JSON.parseObject(oldBodyString); + String encodeString = jsonObject.getString("key"); + + String body; + try { + body = AesEncodeUtil.decrypt(encodeString, key); + } catch (Exception e) { + throw new DecryptFailException("Request parameter decryption failed"); + } + log.debug("解密之后的请求参数:{}", body); + if (StringUtils.isBlank(body)) { + body = "{}"; + } + this.body = body.getBytes(); + } + } + + /** + * 根据 Cookie 名称获取其值 + * + * @param request HttpServletRequest 对象 + * @param name Cookie 名称 + * @return Cookie 的值,如果不存在返回 null + */ + public static String getCookieValue(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + return cookie.getValue(); + } + } + } + return null; + } + +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/http/HttpHelper.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/http/HttpHelper.java new file mode 100644 index 0000000..630b029 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/http/HttpHelper.java @@ -0,0 +1,44 @@ +package com.sonic.sdk.api.utils.http; + +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; + +public class HttpHelper { + + + public static String getBodyString(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + InputStream inputStream = null; + BufferedReader reader = null; + try { + inputStream = request.getInputStream(); + reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); + String line = ""; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return sb.toString(); + } +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/http/RequestBodyWrapper.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/http/RequestBodyWrapper.java new file mode 100644 index 0000000..9a7833e --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/utils/http/RequestBodyWrapper.java @@ -0,0 +1,55 @@ +package com.sonic.sdk.api.utils.http; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; + +public class RequestBodyWrapper extends HttpServletRequestWrapper { + + protected byte[] body; + + public RequestBodyWrapper(HttpServletRequest request) { + super(request); + body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8")); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + + return new ServletInputStream() { + + @Override + public int read() throws IOException { + return bais.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + + } + }; + } +} diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/vo/Result.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/vo/Result.java new file mode 100644 index 0000000..3aab1db --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/vo/Result.java @@ -0,0 +1,88 @@ +package com.sonic.sdk.api.vo; + +import java.io.Serializable; + +public class Result implements Serializable { + private static final long serialVersionUID = 5925101851082556646L; + /** + * 数据对象 + */ + private T content; + /** + * 状态:OK|ERROR + */ + private String status; + /** + * 错误码 + */ + private String errorCode; + /** + * 错误消息 + */ + private String errorMsg; + + public Result() { + this.status = Status.SUCCESS.code(); + } + + public Result(String errorCode, String errorMsg) { + this(errorCode, errorMsg, Status.ERROR); + } + + public Result(String errorCode, String errorMsg, Status status) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + this.status = status.code(); + } + + public T getContent() { + return this.content; + } + + public Result setContent(T content) { + this.content = content; + return this; + } + + public String getStatus() { + return this.status; + } + + public Result setStatus(String status) { + this.status = status; + return this; + } + + public String getErrorCode() { + return this.errorCode; + } + + public Result setErrorCode(String errorCode) { + this.errorCode = errorCode; + return this; + } + + public String getErrorMsg() { + return this.errorMsg; + } + + public Result setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + return this; + } + + public static enum Status { + SUCCESS("OK"), + ERROR("ERROR"); + + private String code; + + private Status(String code) { + this.code = code; + } + + public String code() { + return this.code; + } + } +} \ No newline at end of file diff --git a/sonic-common-api/src/main/java/com/sonic/sdk/api/vo/ServiceAuthRegist.java b/sonic-common-api/src/main/java/com/sonic/sdk/api/vo/ServiceAuthRegist.java new file mode 100644 index 0000000..7453156 --- /dev/null +++ b/sonic-common-api/src/main/java/com/sonic/sdk/api/vo/ServiceAuthRegist.java @@ -0,0 +1,31 @@ +package com.sonic.sdk.api.vo; + +import lombok.Data; +import lombok.ToString; + +import java.util.List; + +/** + * 服务授权注册 + */ +@ToString +@Data +public class ServiceAuthRegist { + /** + * 服务名 对应 spring.application.name + */ + private String applicationName; + /** + * 无需授权的URL + */ + private List ignoreAuthList; + /** + * URL 黑名单 + */ + private List urlBlacklist; + + /** + * 所有的URL名单(只有在此列表中的URL才能被访问到) + */ + private List urlAllList; +} diff --git a/sonic-common-api/src/main/resources/META-INF/spring.factories b/sonic-common-api/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..c081d7c --- /dev/null +++ b/sonic-common-api/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.sonic.sdk.api.utils.SpringContextUtil diff --git a/sonic-common/.gitignore b/sonic-common/.gitignore new file mode 100644 index 0000000..6085469 --- /dev/null +++ b/sonic-common/.gitignore @@ -0,0 +1,27 @@ +/**/rebel.xml + +#target/ +target/ + +# IDEA # +.idea/ +*.iml + +# Eclipse # +.settings/ +.classpath +.project + +# Log # +logs/ +*.log% + +#mac +.DS_Store + +#svn +.svn + +#reviewboardrc +.reviewboardrc + diff --git a/sonic-common/common-lib-box/common-lib/pom.xml b/sonic-common/common-lib-box/common-lib/pom.xml new file mode 100644 index 0000000..89b9035 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/pom.xml @@ -0,0 +1,198 @@ + + + + common-lib-box + com.sonic + 1.0 + + 4.0.0 + + common-lib + 1.0.6 + jar + + + + com.google.guava + guava + + + com.alibaba + fastjson + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-json + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.amqp + spring-amqp + provided + + + org.springframework.amqp + spring-rabbit + provided + + + + org.springframework.boot + spring-boot-starter-data-redis + provided + + + + com.squareup.okhttp3 + okhttp + + + + com.auth0 + java-jwt + + + + org.apache.commons + commons-pool2 + test + + + org.apache.commons + commons-lang3 + + + + org.slf4j + jcl-over-slf4j + + + + log4j-api + org.apache.logging.log4j + + + + + org.slf4j + log4j-over-slf4j + + + + log4j-api + org.apache.logging.log4j + + + + + org.apache.logging.log4j + log4j-to-slf4j + + + + log4j-api + org.apache.logging.log4j + + + + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpclient + + + + com.jayway.jsonpath + json-path + + + + org.springframework + spring-tx + provided + + + + org.yaml + snakeyaml + provided + + + + + io.springfox + springfox-swagger2 + + + io.springfox + springfox-swagger-ui + + + + org.springframework.boot + spring-boot-starter-test + test + + + junit-vintage-engine + org.junit.vintage + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + it.ozimov + embedded-redis + test + + + + + com.amazonaws + aws-java-sdk-cloudfront + 1.12.792 + + + + + + + + + + + + + + + diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/AppRuntime.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/AppRuntime.java new file mode 100644 index 0000000..79d575e --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/AppRuntime.java @@ -0,0 +1,142 @@ +package com.sonic.common; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.Properties; +import java.util.Spliterators; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import com.sonic.common.enums.AppEnv; +import org.springframework.boot.info.GitProperties; +import org.springframework.boot.info.InfoProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; + +import com.alibaba.fastjson.JSONObject; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * @author code + */ +@Data +@Slf4j +public class AppRuntime { + private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final DateTimeFormatter DAY_HOUR_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHH"); + + String appId; + String appName; + AppEnv env; + /** + * 服务启动时间,单位毫秒 + */ + Long startTime; + GitProperties gitProperties; + String hostIP; + JSONObject ext; + + /** + * 唯一标示当前的app实例. 实例重启后会重新 + */ + @Setter(value = AccessLevel.NONE) + String instanceId; + + public String getInstanceId() { + if (instanceId == null) { + instanceId = UUID.randomUUID().toString(); + } + return instanceId; + } + + @Builder + public AppRuntime(String appId, String appName, AppEnv env, ApplicationContext applicationContext) { + Preconditions.checkArgument(env != null || applicationContext != null, "env和applicationContext不能都为空"); + this.appId = appId; + this.appName = appName; + this.env = env; + // 默认构建一个空的gitProperties + this.gitProperties = new GitProperties(new Properties()); + this.startTime = System.currentTimeMillis(); + this.hostIP = "unknown"; + this.ext = new JSONObject(); + try { + this.hostIP = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("failed to get host ip", e); + } + + // 获取applicationContext中保存的信息 + if (applicationContext != null) { + Environment environment = applicationContext.getEnvironment(); + this.env = AppEnv.valueOf(environment.getActiveProfiles()[0]); + if (applicationContext.containsBean("gitProperties")) { + this.gitProperties = applicationContext.getBean("gitProperties", GitProperties.class); + } + } + } + + private String doBuildPrefixKey(Supplier prefixSupplier, Object... keys) { + StringBuilder sb = new StringBuilder(appId).append(":"); + if (env != AppEnv.product) { + sb.append(env.name()).append(":"); + } + if (prefixSupplier != null) { + sb.append(prefixSupplier.get()).append(":"); + } + sb.append(Joiner.on(":").skipNulls().join(keys)); + return sb.toString(); + } + + /** + * @param keys + * @return appid:env:key1:key2 + */ + public String buildPrefixKey(Object... keys) { + return doBuildPrefixKey(null, keys); + } + + /** + * 返回key: appid:env:20190101:key1:key2 + * + * @param keys + * @return appid:env:20190101:key1:key2 + */ + public String buildPrefixDayKey(Object... keys) { + return doBuildPrefixKey(() -> LocalDateTime.now().format(DAY_FORMATTER), keys); + } + + /** + * 返回key: appid:env:2019010105:key1:key2 + * + * @param keys + * @return key: appid:env:2019010105:key1:key2 + */ + public String buildPrefixDayHourKey(Object... keys) { + return doBuildPrefixKey(() -> LocalDateTime.now().format(DAY_HOUR_FORMATTER), keys); + } + + public JSONObject toJson() { + JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(this)); + + // 获取gitProperties中详细的信息,因直接对gitProperties序列化得到的信息不完整,所以需要单独处理 + Map gitPropsMap = StreamSupport.stream( + gitProperties != null ? gitProperties.spliterator() : Spliterators.emptySpliterator(), false) + .collect(Collectors.toMap(InfoProperties.Entry::getKey, InfoProperties.Entry::getValue)); + jsonObject.put("gitProperties", JSONObject.parseObject(JSONObject.toJSONString(gitPropsMap))); + + return jsonObject; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthCaller.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthCaller.java new file mode 100644 index 0000000..388cf68 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthCaller.java @@ -0,0 +1,26 @@ +package com.sonic.common.auth; + +import com.sonic.common.enums.AppEnv; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Rpc调用中调用方对象. 定义了调用方以及调用方环境信息. + * 记录离服务提供者最近的调用信息. 如果调用通过网关转发,这里记录网关信息. + * @author code + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AuthCaller { + String appId; + AppEnv appEnv; + /** + * 自定义扩展信息. + */ + String ext; +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthCallerResolver.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthCallerResolver.java new file mode 100644 index 0000000..84942c5 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthCallerResolver.java @@ -0,0 +1,26 @@ +package com.sonic.common.auth; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * @author code + */ +public class AuthCallerResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter methodParameter) { + return methodParameter.getParameterType().isAssignableFrom(AuthCaller.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + return webRequest.getAttribute(AuthInterceptor.AUTH_PAYLOAD_KEY, RequestAttributes.SCOPE_REQUEST); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthInterceptor.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthInterceptor.java new file mode 100644 index 0000000..a992710 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthInterceptor.java @@ -0,0 +1,133 @@ +package com.sonic.common.auth; + +import java.lang.annotation.Annotation; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.Objects; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.sonic.common.enums.AppEnv; +import com.sonic.common.exception.BizException; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.alibaba.fastjson.JSONObject; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.TokenExpiredException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +/** + * @author code + */ +@Slf4j +@Order(100) +public class AuthInterceptor implements HandlerInterceptor { + + public final static String AUTH_PAYLOAD_KEY = "_auth_payload_"; + public final static String CALLER_PAYLOAD_KEY = "_caller_payload_"; + + private Algorithm algorithm; + + private static final String AUTH_HEADER = "Authorization"; + + private static final String SUPPORT_AUTH = "Bearer"; + + public static final String ORIGINAL_CALLER_HEADER_NAME = "xg-original-caller"; + + public AuthInterceptor(String publicKey) throws Exception { + Objects.requireNonNull(publicKey); + + RSAPublicKey rsaPublicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(BaseEncoding.base64().decode(publicKey))); + + this.algorithm = Algorithm.RSA256(rsaPublicKey, null); + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (!(handler instanceof HandlerMethod)) { + return true; + } + IgnoreAuth ignoreAuth = getAnnotation((HandlerMethod) handler, IgnoreAuth.class); + if (ignoreAuth != null) { + return true; + } + + Token token = Token.from(request.getHeader(AUTH_HEADER)); + if (!StringUtils.equals(SUPPORT_AUTH, token.getType())) { + throw new BizException(AuthResultCode.API_AUTH_NOT_SUPPORTED); + } + + try { + DecodedJWT jwt = JWT.require(algorithm).build() + .verify(token.getToken()); + AuthCaller payload = AuthCaller.builder() + .appId(jwt.getClaim("appId").asString()) + .appEnv(AppEnv.valueOf(jwt.getClaim("appEnv").asString())) + .ext(jwt.getClaim("ext").asString()) + .build(); + + request.setAttribute(AUTH_PAYLOAD_KEY, payload); + + OriginalCaller caller; + String originalCallerHeader = request.getHeader(ORIGINAL_CALLER_HEADER_NAME); + if (!Strings.isNullOrEmpty(originalCallerHeader)) { + caller = JSONObject.parseObject(originalCallerHeader, OriginalCaller.class); + } else { + caller = OriginalCaller.fromAuthCaller(payload); + } + request.setAttribute(CALLER_PAYLOAD_KEY, caller); + } catch (TokenExpiredException e) { + response.setStatus(com.sonic.common.rpc.exception.TokenExpiredException.STATUS_CODE); + throw new BizException(AuthResultCode.API_AUTH_TOKEN_EXPIRED); + } catch (Exception e) { + log.error("api auth, token is invalid. token = {}", token, e); + throw new BizException(AuthResultCode.API_AUTH_TOKEN_INVALID); + } + + return true; + } + + private T getAnnotation(HandlerMethod handlerMethod, Class clazz) { + T annotation = handlerMethod.getMethodAnnotation(clazz); + if (annotation != null) { + return annotation; + } + annotation = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), clazz); + return annotation; + } + + @Data + @Builder + @AllArgsConstructor + @ToString + private static class Token { + private static final transient String DELIMITER = StringUtils.SPACE; + + private String type; + + private String token; + + public static Token from(String header) { + return Token.builder() + .type(StringUtils.substringBefore(header, DELIMITER)) + .token(StringUtils.substringAfter(header, DELIMITER)) + .build(); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthResultCode.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthResultCode.java new file mode 100644 index 0000000..f78f5e9 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/AuthResultCode.java @@ -0,0 +1,32 @@ +package com.sonic.common.auth; + +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.rpc.GlobalResultCode; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author code + */ +@AllArgsConstructor +@Getter +public enum AuthResultCode implements ApiResultCode { + + /** + * 前 4 位为服务id,全局 id 为 "0000",errorCode 的区间范围在 000 -> 099 + */ + API_AUTH_NOT_SUPPORTED("0000100", "不支持的认证方式"), + API_AUTH_TOKEN_EXPIRED("0000101", "Token过期"), + API_AUTH_TOKEN_INVALID("0000102", "Token不合法"), + ; + + private String errorCode; + private String errorMsg; + + @Override + public String getAppId() { + return GlobalResultCode.GLOBAL_APP_ID; + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/DummyAuthInterceptor.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/DummyAuthInterceptor.java new file mode 100644 index 0000000..ad8ef02 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/DummyAuthInterceptor.java @@ -0,0 +1,72 @@ +package com.sonic.common.auth; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.google.common.base.Strings; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +/** + * 为测试环境准备的dummy认证检查拦截器. + * @author code + */ +@Slf4j +public class DummyAuthInterceptor implements HandlerInterceptor { + + private static final String AUTH_HEADER = "Authorization"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (!(handler instanceof HandlerMethod)) { + return true; + } + // 对于dummy的token,携带的信息是明文传输的 + String authorization = request.getHeader(AUTH_HEADER); + if (Strings.isNullOrEmpty(authorization)) { + return true; + } + Token token = Token.from(authorization); + AuthCaller authCaller = JSON.parseObject(token.getToken(), AuthCaller.class); + request.setAttribute(AuthInterceptor.AUTH_PAYLOAD_KEY, authCaller); + + OriginalCaller caller; + String originalCallerHeader = request.getHeader(AuthInterceptor.ORIGINAL_CALLER_HEADER_NAME); + if (!Strings.isNullOrEmpty(originalCallerHeader)) { + caller = JSONObject.parseObject(originalCallerHeader, OriginalCaller.class); + } else { + caller = OriginalCaller.fromAuthCaller(authCaller); + } + request.setAttribute(AuthInterceptor.CALLER_PAYLOAD_KEY, caller); + return true; + } + + @Data + @Builder + @AllArgsConstructor + @ToString + private static class Token { + private static final transient String DELIMITER = StringUtils.SPACE; + + private String type; + + private String token; + + public static Token from(String header) { + return Token.builder() + .type(StringUtils.substringBefore(header, DELIMITER)) + .token(StringUtils.substringAfter(header, DELIMITER)) + .build(); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/GateWaySessionInterceptor.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/GateWaySessionInterceptor.java new file mode 100644 index 0000000..d1da90c --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/GateWaySessionInterceptor.java @@ -0,0 +1,148 @@ +package com.sonic.common.auth; + +import com.alibaba.fastjson.JSON; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.utils.IpAddressUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + + +/** + * 解析请求头 得到 session 对象 + * 也可以模拟 x-id 构造一个对象 + */ +@Slf4j +public class GateWaySessionInterceptor implements HandlerInterceptor { + + public static final String REQUEST_ATTR_SESSION = "_session_"; + private static final String DEBUG_USER_ID = "x-id"; + private static final String GATE_WAY_SESSION_JSON = "gateway-session-json"; + + /** + * 只有移动端在传 + */ + private static final String PLATFORM = "platform"; + private static final String VERSION_NUM = "versionNum"; + private static final String USER_AGENT = "User-Agent"; + /**通过header头传过来的设备号*/ + private static final String DEVICE_ID = "AUTH_DID"; + /**通过cookie传过来的设备号*/ + private static final String COOKIE_DEVICE_ID_KEY = "COOKIE_DID"; + + @Value("${session.interceptor.skip:false}") + private String skipSessionInterceptor; + + @Value("${spring.profiles.active}") + private String runMode; + + /** + * 环境变量列表 + */ + private static final List devList = new ArrayList<>(); + { + devList.add("local"); + devList.add("dev"); + devList.add("test"); + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws UnsupportedEncodingException { + request.setCharacterEncoding("UTF-8"); + response.setCharacterEncoding("UTF-8"); + if (!(handler instanceof HandlerMethod)) { + log.info("GateWaySessionInterceptor no instanceof"); + return true; + } + String json = request.getHeader(GATE_WAY_SESSION_JSON); + log.info("GateWaySessionInterceptor json:{}", json); + String deviceId = getValue(request, DEVICE_ID, COOKIE_DEVICE_ID_KEY); + String ip = IpAddressUtils.getIpAddress(request);; + String versionNum = getValue(request, VERSION_NUM, VERSION_NUM); + String platform = getValue(request, PLATFORM, PLATFORM); + //当使用mock的用户ID时只能在指定的环境生效(local/dev/test) + if ("true".equals(skipSessionInterceptor) && devList.contains(runMode) && StringUtils.isBlank(json)) { + String userId = request.getHeader(DEBUG_USER_ID); + Session session = new Session(); + session.setUserId(StringUtils.isEmpty(userId) ? null : Long.valueOf(userId)); + //设置设备信息 + session.setDeviceCode(deviceId); + session.setIp(ip); + session.setVersionNum(StringUtils.isEmpty(versionNum) ? 0 : Integer.valueOf(versionNum)); + session.setEndpoint(platform); + //浏览器基础信息 + session.setUserAgent(request.getHeader(USER_AGENT)); + request.setAttribute(REQUEST_ATTR_SESSION, session); + return true; + } + Session session = new Session(); + if (StringUtils.isNotBlank(json)) { + session = JSON.parseObject(json, Session.class); + } + //设置设备信息 + session.setDeviceCode(deviceId); + session.setIp(ip); + session.setVersionNum(StringUtils.isEmpty(versionNum) ? 0 : Integer.valueOf(versionNum)); + session.setEndpoint(platform); + //设置语言、浏览器基础信息 + session.setUserAgent(request.getHeader(USER_AGENT)); + log.info("GateWaySessionInterceptor session:{}", session); + request.setAttribute(REQUEST_ATTR_SESSION, session); + return true; + } + + @Override + public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { + + } + + @Override + public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { + + } + + /** + * 首先从header头中获取值,没有获取到再从cooke获取 + * @param request + * @param key1 + * @param key2 + * @return + */ + private String getValue(HttpServletRequest request, String key1, String key2) { + String value = request.getHeader(key1); + if(StringUtils.isEmpty(value)) { + value = getCookieValue(request, key2); + } + return value; + } + + /** + * 根据 Cookie 名称获取其值 + * + * @param request HttpServletRequest 对象 + * @param name Cookie 名称 + * @return Cookie 的值,如果不存在返回 null + */ + private String getCookieValue(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + return cookie.getValue(); + } + } + } + return null; + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/IgnoreAuth.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/IgnoreAuth.java new file mode 100644 index 0000000..aae2d86 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/IgnoreAuth.java @@ -0,0 +1,18 @@ +package com.sonic.common.auth; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 忽略调用认证的annotation.通过这个annotation来指定接口不做调用的认证检查. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface IgnoreAuth { +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/OriginalCaller.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/OriginalCaller.java new file mode 100644 index 0000000..49eaffc --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/OriginalCaller.java @@ -0,0 +1,24 @@ +package com.sonic.common.auth; + +import org.springframework.beans.BeanUtils; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 因为调用会经过网关转发, 而我们更关注的是原始调用服务的信息. + * 所有OriginalCaller记录调用服务的原始调用方对象信息. + * 这些信息会放在Http Header: xg-original-caller + * @author code + */ +@EqualsAndHashCode(callSuper = true) +@Data +@AllArgsConstructor +public class OriginalCaller extends AuthCaller { + public static OriginalCaller fromAuthCaller(AuthCaller authCaller) { + OriginalCaller originalCaller = new OriginalCaller(); + BeanUtils.copyProperties(authCaller, originalCaller); + return originalCaller; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/OriginalCallerResolver.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/OriginalCallerResolver.java new file mode 100644 index 0000000..4119cac --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/OriginalCallerResolver.java @@ -0,0 +1,27 @@ +package com.sonic.common.auth; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * @author code + */ +public class OriginalCallerResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter methodParameter) { + return methodParameter.getParameterType().isAssignableFrom(OriginalCaller.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + return webRequest.getAttribute(AuthInterceptor.CALLER_PAYLOAD_KEY, RequestAttributes.SCOPE_REQUEST); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/SessionResolver.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/SessionResolver.java new file mode 100644 index 0000000..c216341 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/SessionResolver.java @@ -0,0 +1,29 @@ +package com.sonic.common.auth; + +import com.sonic.common.auth.domains.Session; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.util.Optional; + + +public class SessionResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter methodParameter) { + return methodParameter.getParameterType().isAssignableFrom(Session.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + // 为了兼容同时支持已登录和未登录都能访问的接口,这里初始化一个无数据对象避免 NPE + return Optional.ofNullable(webRequest.getAttribute("_session_", RequestAttributes.SCOPE_REQUEST)).orElseGet(Session::new); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/domains/AppClientEnum.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/domains/AppClientEnum.java new file mode 100644 index 0000000..d49b3fa --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/domains/AppClientEnum.java @@ -0,0 +1,31 @@ +package com.sonic.common.auth.domains; + +import lombok.Getter; + +/** + * 应用端编码,数据来自 app_client.code + * @author code + */ +@Getter +public enum AppClientEnum { + + /** + * web 端 + */ + WEB, + /** + * 管理后台 + */ + ADMIN, + /** + * ios 端 + */ + IOS, + + /** + * android 端 + */ + ANDROID, + ; + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/domains/Session.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/domains/Session.java new file mode 100644 index 0000000..17b11ed --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/domains/Session.java @@ -0,0 +1,93 @@ +package com.sonic.common.auth.domains; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author code + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Session implements Serializable { + /** + * 授权token + */ + private String token; + /** + * 用户ID + */ + private Long userId; + /** + * 用户ID + */ + private String idCard; + /** + * session状态 + */ + private SessionStatusEnum status; + /** + * session创建时间 + */ + private LocalDateTime createTime; + /** + * session最后访问时间 + */ + private LocalDateTime lastAccessTime; + /** + * session过期时间 + */ + private LocalDateTime expireTime; + /** + * 用户注册时间 + */ + private LocalDateTime registerTime; + /** + * 终端类型【前端透传 WEB、ANDROID、IOS】 + */ + private String endpoint; + + /** + * 数字版本号【前端透传 数字版本号】 + */ + private Integer versionNum; + + /** + * 终端设备唯一识别码【前端透传】 + */ + private String deviceCode; + + /** + * 终端设备IP【前端透传】 + */ + private String ip; + + /** + * 终端设备UA【前端透传】 + */ + private String userAgent; + + + public boolean isEnabled() { + return status == null || status == SessionStatusEnum.ENABLED; + } + + public boolean isDisabled() { + return status != null && status != SessionStatusEnum.ENABLED; + } + + public boolean isExpired() { + return expireTime != null && LocalDateTime.now().isAfter(expireTime); + } + + public boolean isOverMaxAlive(Integer sessionAlive) { + LocalDateTime maxAliveTime = createTime.plusMinutes(sessionAlive); + return LocalDateTime.now().isAfter(maxAliveTime); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/domains/SessionStatusEnum.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/domains/SessionStatusEnum.java new file mode 100644 index 0000000..70b8aff --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/auth/domains/SessionStatusEnum.java @@ -0,0 +1,21 @@ +package com.sonic.common.auth.domains; + +/** + * @Author zzhan + * @Description 用户 session 状态,默认为有效 + * @Date 2023/11/10 15:56 + * @Version 1.0 + */ +public enum SessionStatusEnum { + + /** 有效 */ + ENABLED, + /** 已过期 */ + EXPIRED, + /** 已登出 */ + LOGOUT, + /** 踢下线 */ + KICK_OUT + ; + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/AbstractBsClient.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/AbstractBsClient.java new file mode 100644 index 0000000..2eea39e --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/AbstractBsClient.java @@ -0,0 +1,92 @@ +package com.sonic.common.client; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.sonic.common.AppRuntime; +import com.sonic.common.enums.AppEnv; +import com.sonic.common.rpc.RpcClient; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +/** + * @author chenjun + */ +public abstract class AbstractBsClient { + + @Setter + protected AppCenterClient appCenterClient; + @Setter + protected String appId; + @Setter + protected AppEnv appEnv; + @Setter + protected RpcClient rpcClient; + /** + * 目前host是直接指定的网关的host。请求通过网关转发 + */ + @Setter + protected String host; + + @Data + public abstract static class Builder { + protected AppRuntime appRuntime; + protected RpcClient rpcClient; + protected boolean isDummy; + protected AppCenterClient appCenterClient; + protected String host = ""; + + protected abstract AbstractBsClient createService(); + + protected AbstractBsClient buildService() { + if (!isDummy) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(appRuntime.getAppId()), "缺少appId"); + Preconditions.checkArgument(appRuntime.getEnv() != null, "缺少appEnv"); + Preconditions.checkArgument(rpcClient != null, "缺少 rpcClient"); + Preconditions.checkArgument(appCenterClient != null, "缺少appCenterClient"); + } + + AbstractBsClient service = createService(); + service.setAppId(appRuntime.getAppId()); + service.setAppEnv(appRuntime.getEnv()); + service.setRpcClient(rpcClient); + service.setAppCenterClient(appCenterClient); + service.setHost(host); + return service; + } + } + + /** + * 获取服务地址前缀(通过网关访问后端服务)
+ * ${gatewayHost+serviceName}
+ * sample: http://bs-gateway.gamers.gg/bs-app-center
+ * + * @return + */ + protected String getPathPrefix() { + return (host.endsWith("/") ? host : host + "/") + appCenterClient.getNameByAppId(serviceAppId()); + } + + /** + * app-center维护的服务的app.id + * + * @return + */ + protected abstract String serviceAppId(); + + /** + * 定义一个预置服务的枚举,方便客户端内部使用
+ * 该枚举仅用于提供内置服务的appId,方便阅读。 + */ + @Getter + protected enum BuildInServiceEnum { + APP_CENTER("0001"), + ; + + BuildInServiceEnum(String appId) { + this.appId = appId; + } + + private String appId; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/AppCenterClient.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/AppCenterClient.java new file mode 100644 index 0000000..9424f25 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/AppCenterClient.java @@ -0,0 +1,86 @@ +package com.sonic.common.client; + +import com.sonic.common.enums.AppEnv; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * @author chenjun + */ +public interface AppCenterClient { + /** + * 获取token,rpcClient本身已经集成了获取token的过程,该方法一般不会用到 + */ + String token(); + + /** + * 获取App列表 + * + * @param listReq 查询参数 + * @return + */ + List list(ListReq listReq); + + /** + * 根据 appId 获取 App 信息 + * + * @param appId + * @return + */ + Optional getById(String appId); + + /** + * 根据appEnv,appId,获取对应host,其中,appEnv在client构建时已经指定 + * + * @param appId + * @return + */ + String getNameByAppId(String appId); + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + class ListReq { + @NotEmpty + List appIds; + AppEnv appEnv; + String tag; + } + + @NoArgsConstructor + @Data + @Builder + @AllArgsConstructor + class App { + private String id; + private String displayName; + private String serviceName; + private String host; + private String steinName; + private AccessType accessType; + private String owner; + private String dept; + private Status status; + private Set tags; + private String ext; + private String deptEmails; + } + + enum AccessType { + PUBLIC, PRIVATE + } + + enum Status { + ENABLED, + DISABLED, + ; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/BsClientFactory.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/BsClientFactory.java new file mode 100644 index 0000000..ebb473f --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/BsClientFactory.java @@ -0,0 +1,83 @@ +package com.sonic.common.client; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.sonic.common.AppRuntime; +import com.sonic.common.enums.AppEnv; +import com.sonic.common.rpc.RpcClient; +import lombok.Data; + +public class BsClientFactory { + + private AppRuntime appRuntime; + private RpcClient rpcClient; + private boolean isDummy; + private AppCenterClient appCenterClient; + private String host; + + private BsClientFactory() { + // used to prevent creating instance directly + } + + // public identifier if other client not build in bs-client, can also use resolveClientBuilder + public T resolveClientBuilder(T builder) { + builder.setAppRuntime(appRuntime); + builder.setRpcClient(rpcClient); + builder.setDummy(isDummy); + builder.setAppCenterClient(appCenterClient); + builder.setHost(host); + return builder; + } + + public static Builder builder() { + return new Builder(); + } + + @Data + public static class Builder { + private AppRuntime appRuntime; + private RpcClient rpcClient; + private boolean isDummy; + private AppCenterClient appCenterClient; + private String host = ""; + + public Builder appRuntime(AppRuntime appRuntime) { + this.appRuntime = appRuntime; + return this; + } + + public Builder rpcClient(RpcClient rpcClient) { + this.rpcClient = rpcClient; + return this; + } + + public Builder appCenterClient(AppCenterClient appCenterClient) { + this.appCenterClient = appCenterClient; + return this; + } + + public Builder host(String host) { + this.host = Strings.nullToEmpty(host); + return this; + } + + public BsClientFactory build() { + Preconditions.checkArgument(!Strings.isNullOrEmpty(appRuntime.getAppId()), "缺少appId"); + Preconditions.checkArgument(appRuntime.getEnv() != null, "缺少 appEnv"); + Preconditions.checkArgument(rpcClient != null, "缺少 rpcClient"); + Preconditions.checkArgument(appCenterClient != null, "缺少 appCenterClient"); + + BsClientFactory factory = new BsClientFactory(); + factory.appRuntime = appRuntime; + if (appRuntime.getEnv() == AppEnv.unittest) { + factory.isDummy = true; + return factory; + } + + factory.rpcClient = rpcClient; + factory.appCenterClient = appCenterClient; + factory.host = host; + return factory; + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/JobmanClient.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/JobmanClient.java new file mode 100644 index 0000000..f7b5dfb --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/JobmanClient.java @@ -0,0 +1,136 @@ +package com.sonic.common.client; + +import com.gamers.bs.client.util.ParameterFormatter; +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +public interface JobmanClient { + + /** + * 执行Job. 并使用{@code lockPolicy}的锁策略. + * + * @param jobName + * @param lockPolicy 锁策略, {@link JobmanClient.LockPolicy} + * @param runner 任务逻辑 + */ + void run(String jobName, LockPolicy lockPolicy, Function runner); + + /** + * 执行Job. 优先使用globalLockPolicy, 没有情况下默认使用redis的锁策略. 并将锁持有{@code lockDurationSeconds}秒. 在该时间内都将由本实例执行job + * + * @param jobName + * @param lockDurationSeconds 锁持有时间, 在持有时间内job由该实例调度. 单位秒 + * @param runner 任务逻辑 + */ + void run(String jobName, Long lockDurationSeconds, Function runner); + + /** + * 执行job, 优先使用globalLockPolicy, 没有情况下默认使用redis的锁策略. 默认持有锁5分钟 + * + * @param jobName + * @param runner + */ + default void run(String jobName, Function runner) { + run(jobName, TimeUnit.MINUTES.toSeconds(5), runner); + } + + /** + * 用于判断 @{code dateTime} 是否满足 @{code cornExpression}的执行条件 + * + * @param cornExpression + * @return + */ + boolean isCronSatisfied(String cornExpression, LocalDateTime dateTime); + + @Slf4j + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @ToString(exclude = "logs") + class JobContext { + String jobName; + @Builder.Default + List logs = Lists.newArrayList(); + LocalDateTime startTime; + LocalDateTime endTime; + + public void log(String message, Object... objects) { + String logText = objects == null ? message : ParameterFormatter.format(message, objects); + if (objects != null && objects[objects.length - 1] instanceof Throwable) { + logText += "\n" + Throwables.getStackTraceAsString((Throwable) objects[objects.length - 1]); + log.error("=== running job ===, jobName = {}, log = {}", jobName, logText); + } else { + log.info("=== running job ===, jobName = {}, log = {}", jobName, logText); + } + + logs.add(logText); + } + + public void log(String message) { + log(message, null); + } + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @ToString + class JobResult { + @Builder.Default + JobStatus jobStatus = JobStatus.SUCCESS; + @Builder.Default + Integer loadCount = 0; + Map ext; + + public static JobResult success(Integer loadCount) { + return JobResult.builder() + .loadCount(loadCount) + .jobStatus(loadCount > 0 ? JobStatus.SUCCESS : JobStatus.EMPTY) + .build(); + } + + public static JobResult fail() { + return JobResult.builder().jobStatus(JobStatus.FAIL).build(); + } + + public static JobResult empty() { + return JobResult.builder().jobStatus(JobStatus.EMPTY).build(); + } + } + + @Getter + @AllArgsConstructor + enum JobStatus { + SUCCESS, + FAIL, + //如果没有数据更新, 状态设置为空 + EMPTY, + ; + } + + interface LockPolicy { + /** + * 尝试获得锁, 返回true则可以获得锁并执行业务逻辑. false不会继续执行业务逻辑 + * 调用者可以实现该接口以自定义策略 + * + * @return + */ + boolean tryLock(); + + /** + * 容器销毁时回调方法. 用于清理LockPolicy的资源. + */ + default void destroy() { + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/TimerRefreshCache.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/TimerRefreshCache.java new file mode 100644 index 0000000..e736974 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/TimerRefreshCache.java @@ -0,0 +1,92 @@ +package com.sonic.common.client; + +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import com.google.common.base.Preconditions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 间隔一定时间刷新的缓存。 + *

+ * 1、初次调用时,直接获取需要缓存的内容,并缓存到cache中
+ * 2、每间隔intervalMillis尝试刷新缓存,
+ *      如果刷新缓存成功,更新cache缓存的值
+ *      如果刷新缓存失败,则不更新
+ * 
+ * + * @param + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Slf4j +public class TimerRefreshCache { + private ScheduledThreadPoolExecutor executor; + private String name; + private Long initialDelayMillis; + private Long intervalMillis; + private Supplier refresher; + private Cache cache; + /** 缓存是否以初始化,如果为false,会进行主动加载进行初始化 */ + private AtomicBoolean initialized = new AtomicBoolean(false); + + public TimerRefreshCache(String name, Long initialDelayMillis, Long intervalMillis, + ScheduledThreadPoolExecutor executor, Supplier refresher) { + Preconditions.checkArgument(executor != null); + Preconditions.checkArgument(refresher != null); + Preconditions.checkArgument(intervalMillis != null); + + this.name = name; + this.initialDelayMillis = initialDelayMillis; + this.intervalMillis = intervalMillis; + this.refresher = refresher; + // 该方法获取的,往往是完整的缓存内容。所以maxSize暂定为2即可 + long maximumSize = 2L; + cache = CacheBuilder.newBuilder() + .maximumSize(maximumSize) + .build(); + this.executor = executor; + startTimer(); + } + + private void startTimer() { + executor.scheduleAtFixedRate(this::doRefresh, initialDelayMillis, intervalMillis, TimeUnit.MILLISECONDS); + } + + private void doRefresh() { + try { + T value = refresher.get(); + // 当返回为null,表示Value没有变化,这时不用刷新Cache + if (value == null) { + log.debug("{} already has latest value.", name); + return; + } + + cache.put(name, value); + log.info("{} refreshed, new value={}", name, value); + } catch (Throwable e) { + log.error("{} refresh failed", name, e); + } + } + + public T get() { + // 如果没有加载过,手动加载一次 + if (!initialized.getAndSet(true)) { + // 如果get的值不为空,放到cache中 + doRefresh(); + } + return cache.getIfPresent(name); + } + + // XXX: for unittest + public void put(T configs) { + log.info("{},set->config={}", TimerRefreshCache.this, configs); + cache.put(name, configs); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/impl/AppCenterClientImpl.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/impl/AppCenterClientImpl.java new file mode 100644 index 0000000..5244063 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/impl/AppCenterClientImpl.java @@ -0,0 +1,263 @@ +package com.sonic.common.client.impl; + +import com.alibaba.fastjson.JSON; +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.AuthCaller; +import com.sonic.common.client.AppCenterClient; +import com.sonic.common.client.TimerRefreshCache; +import com.sonic.common.enums.AppEnv; +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.GlobalResultCode; +import com.sonic.common.rpc.RpcClient; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author chenjun + */ +public class AppCenterClientImpl implements AppCenterClient { + + /** load cache immediately */ + private static final long INITIAL_DELAY_MILLIS = 0; + private static final long REFRESH_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(10); + + private static final String TOKEN_PATH = "/app/token"; + private static final String LIST = "/app/list"; + + private String host; + private String secret; + private RpcClient rpcClient; + private AppRuntime appRuntime; + private BiConsumer alertConsumer; + /** + * 支持外部提供app列表,当appCenter返回的app与外部提供的app有相同的appId或serviceName时,优先使用外部提供的app + */ + private Supplier> appSupplier; + + private TimerRefreshCache> appCache; + + @Override + public String token() { + LocalDateTime now = LocalDateTime.now(); + TokenReq req = TokenReq.builder() + .appId(appRuntime.getAppId()).appEnv(appRuntime.getEnv()).timestamp(now) + .signature(buildSignature(secret, null, now)).build(); + return rpcClient.request() + .url(host + TOKEN_PATH) + .content(req) + .clz(String.class) + .post(); + } + + @Override + public List list(ListReq listReq) { + List apps = rpcClient.request() + .url(host + LIST) + .content(listReq) + .clz(App.class) + .postAndGetList(); + if (appSupplier == null) { + return apps; + } + // 如果有指定的supplier, 使用指定的supplier获取的app替换app center返回的app + List suppliedApps = appSupplier.get().stream() + .filter(a -> !Strings.isNullOrEmpty(a.getId()) + && !Strings.isNullOrEmpty(a.getServiceName()) + && !Strings.isNullOrEmpty(a.getHost())) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(suppliedApps)) { + return apps; + } + + Set suppliedAppIds = suppliedApps.stream().map(App::getId) + .filter(Objects::nonNull).collect(Collectors.toSet()); + Set suppliedServiceNames = suppliedApps.stream().map(App::getServiceName) + .filter(Objects::nonNull).collect(Collectors.toSet()); + // 过滤appCenter返回的相同appId或serviceName的app,并添加外部提供的app列表,便于在本地测试时 + // 可以使用本地的配置替换appCenter中的配置 + return Stream.concat(apps.stream().filter(app -> !suppliedAppIds.contains(app.getId()) + && !suppliedServiceNames.contains(app.getServiceName())), suppliedApps.stream()) + .collect(Collectors.toList()); + } + + @Override + public Optional getById(String appId) { + return Optional.ofNullable(appCache.get()).map(m -> m.get(appId)); + } + + @Override + public String getNameByAppId(String appId) { + return getById(appId) + .map(App::getServiceName) + .orElseThrow(() -> new BizException( + GlobalResultCode.INVALID_PARAMS.getErrorCode(), String.format("指定的appId尚未在AppCenter注册, appId = %s", appId))); + } + + private Map loadAllAppHosts() { + ListReq req = ListReq.builder().appEnv(appRuntime.getEnv()).build(); + try { + // supplier提供的App可能没有id,需要过滤 + List list = list(req).stream().filter(a -> !Strings.isNullOrEmpty(a.getId())).collect(Collectors.toList()); + return Maps.uniqueIndex(list, App::getId); + } catch (Exception e) { + postAlert("AppCenterClient拉取app列表失败, req = {}", req, e); + throw e; + } + } + + private void postAlert(String message, Object... params) { + if (alertConsumer != null) { + alertConsumer.accept(message, params); + } + } + + private AppCenterClientImpl(Builder builder) { + this.rpcClient = builder.getRpcClient(); + this.host = builder.getHost(); + this.appRuntime = builder.getAppRuntime(); + this.secret = builder.getSecret(); + this.alertConsumer = builder.getAlertConsumer(); + this.appSupplier = builder.getAppSupplier(); + this.appCache = new TimerRefreshCache<>("appCache", INITIAL_DELAY_MILLIS, + REFRESH_INTERVAL_MILLIS, builder.getExecutor(), this::loadAllAppHosts); + + Preconditions.checkArgument(!Strings.isNullOrEmpty(appRuntime.getAppId()), "缺少appId"); + Preconditions.checkArgument(appRuntime.getEnv() != null, "缺少appEnv"); + Preconditions.checkArgument(rpcClient != null, "缺少 rpcClient"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(secret), "缺少 secret"); + } + + public static Builder builder() { + return new Builder(); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Builder { + private String secret; + @Deprecated + private String host = ""; + private AppRuntime appRuntime; + private RpcClient rpcClient; + private ScheduledThreadPoolExecutor executor; + private BiConsumer alertConsumer; + private Supplier> appSupplier; + + public Builder secret(String secret) { + this.secret = secret; + return this; + } + + public Builder host(String host) { + this.host = Strings.nullToEmpty(host); + return this; + } + + public Builder appRuntime(AppRuntime appRuntime) { + this.appRuntime = appRuntime; + return this; + } + + public Builder rpcClient(RpcClient rpcClient) { + this.rpcClient = rpcClient; + return this; + } + + public Builder executor(ScheduledThreadPoolExecutor executor) { + this.executor = executor; + return this; + } + + public Builder alertConsumer(BiConsumer alertConsumer) { + this.alertConsumer = alertConsumer; + return this; + } + + public Builder appSupplier(Supplier> appSupplier) { + this.appSupplier = appSupplier; + return this; + } + + public AppCenterClient build() { + Preconditions.checkArgument(executor != null, "executor不能为空"); + Preconditions.checkArgument(appRuntime != null, "appRuntime不能为空"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(appRuntime.getAppId()), "缺少appId"); + Preconditions.checkArgument(appRuntime.getEnv() != null, "缺少appEnv"); + Preconditions.checkArgument(rpcClient != null, "缺少 rpcClient"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(secret), "缺少 secret"); + + if (appRuntime.getEnv() == AppEnv.unittest) { + return buildDummyAppCenterClient(); + } else { + return new AppCenterClientImpl(this); + } + } + + private AppCenterClientImpl buildDummyAppCenterClient() { + // build dummy implement for app center client + return new AppCenterClientImpl(this) { + @Override + public String token() { + return JSON.toJSONString(AuthCaller.builder() + .appId(appRuntime.getAppId()).appEnv(appRuntime.getEnv()).build()); + } + + @Override + public List list(ListReq listReq) { + return ImmutableList.of(App.builder().id("0001").displayName("测试") + .serviceName("test").status(Status.ENABLED) + .dept("bs") + .host("http://localhost:8080").build()); + } + + @Override + public String getNameByAppId(String appId) { + return "test"; + } + }; + } + } + + @lombok.Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + static class TokenReq { + private String appId; + + private LocalDateTime timestamp; + + private String signature; + private String ext; + private AppEnv appEnv; + } + + private String buildSignature(String secret, String ext, LocalDateTime timestamp) { + return BaseEncoding.base64().encode(Hashing.sha256().newHasher() + .putString(appRuntime.getAppId() + appRuntime.getEnv().name() + + ext + timestamp.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + secret, Charsets.UTF_8) + .hash() + .asBytes()); + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/impl/CronExpression.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/impl/CronExpression.java new file mode 100644 index 0000000..3b3afba --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/impl/CronExpression.java @@ -0,0 +1,1509 @@ +package com.sonic.common.client.impl; + +import java.io.Serializable; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.TreeSet; + +/** + * corn 表达式的工具类. 用于判断表达式是否match时间, 校验表达式是否正确等 + * copy from org.quartz.CronExpression + *

+ * {@link + */ + +class CronExpression implements Serializable, Cloneable { + + private static final long serialVersionUID = 12423409423L; + + protected static final int SECOND = 0; + protected static final int MINUTE = 1; + protected static final int HOUR = 2; + protected static final int DAY_OF_MONTH = 3; + protected static final int MONTH = 4; + protected static final int DAY_OF_WEEK = 5; + protected static final int YEAR = 6; + protected static final int ALL_SPEC_INT = 99; // '*' + protected static final int NO_SPEC_INT = 98; // '?' + protected static final Integer ALL_SPEC = ALL_SPEC_INT; + protected static final Integer NO_SPEC = NO_SPEC_INT; + + protected static final Map monthMap = new HashMap(20); + protected static final Map dayMap = new HashMap(60); + + static { + monthMap.put("JAN", 0); + monthMap.put("FEB", 1); + monthMap.put("MAR", 2); + monthMap.put("APR", 3); + monthMap.put("MAY", 4); + monthMap.put("JUN", 5); + monthMap.put("JUL", 6); + monthMap.put("AUG", 7); + monthMap.put("SEP", 8); + monthMap.put("OCT", 9); + monthMap.put("NOV", 10); + monthMap.put("DEC", 11); + + dayMap.put("SUN", 1); + dayMap.put("MON", 2); + dayMap.put("TUE", 3); + dayMap.put("WED", 4); + dayMap.put("THU", 5); + dayMap.put("FRI", 6); + dayMap.put("SAT", 7); + } + + private final String cronExpression; + private TimeZone timeZone = null; + protected transient TreeSet seconds; + protected transient TreeSet minutes; + protected transient TreeSet hours; + protected transient TreeSet daysOfMonth; + protected transient TreeSet months; + protected transient TreeSet daysOfWeek; + protected transient TreeSet years; + + protected transient boolean lastdayOfWeek = false; + protected transient int nthdayOfWeek = 0; + protected transient boolean lastdayOfMonth = false; + protected transient boolean nearestWeekday = false; + protected transient int lastdayOffset = 0; + protected transient boolean expressionParsed = false; + + public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; + + /** + * Constructs a new CronExpression based on the specified + * parameter. + * + * @param cronExpression String representation of the cron expression the + * new object should represent + * @throws ParseException if the string expression cannot be parsed into a valid + * CronExpression + */ + public CronExpression(String cronExpression) throws ParseException { + if (cronExpression == null) { + throw new IllegalArgumentException("cronExpression cannot be null"); + } + + this.cronExpression = cronExpression.toUpperCase(Locale.US); + + buildExpression(this.cronExpression); + } + + /** + * Constructs a new {@code CronExpression} as a copy of an existing + * instance. + * + * @param expression The existing cron expression to be copied + */ + public CronExpression(CronExpression expression) { + /* + * We don't call the other constructor here since we need to swallow the + * ParseException. We also elide some of the sanity checking as it is + * not logically trippable. + */ + this.cronExpression = expression.getCronExpression(); + try { + buildExpression(cronExpression); + } catch (ParseException ex) { + throw new AssertionError(); + } + if (expression.getTimeZone() != null) { + setTimeZone((TimeZone) expression.getTimeZone().clone()); + } + } + + /** + * Indicates whether the given date satisfies the cron expression. Note that + * milliseconds are ignored, so two Dates falling on different milliseconds + * of the same second will always have the same result here. + * + * @param date the date to evaluate + * @return a boolean indicating whether the given date satisfies the cron + * expression + */ + public boolean isSatisfiedBy(Date date) { + Calendar testDateCal = Calendar.getInstance(getTimeZone()); + testDateCal.setTime(date); + testDateCal.set(Calendar.MILLISECOND, 0); + Date originalDate = testDateCal.getTime(); + + testDateCal.add(Calendar.SECOND, -1); + + Date timeAfter = getTimeAfter(testDateCal.getTime()); + + return ((timeAfter != null) && (timeAfter.equals(originalDate))); + } + + /** + * Returns the next date/time after the given date/time which + * satisfies the cron expression. + * + * @param date the date/time at which to begin the search for the next valid + * date/time + * @return the next valid date/time + */ + public Date getNextValidTimeAfter(Date date) { + return getTimeAfter(date); + } + + /** + * Returns the next date/time after the given date/time which does + * not satisfy the expression + * + * @param date the date/time at which to begin the search for the next + * invalid date/time + * @return the next valid date/time + */ + public Date getNextInvalidTimeAfter(Date date) { + long difference = 1000; + + //move back to the nearest second so differences will be accurate + Calendar adjustCal = Calendar.getInstance(getTimeZone()); + adjustCal.setTime(date); + adjustCal.set(Calendar.MILLISECOND, 0); + Date lastDate = adjustCal.getTime(); + + Date newDate; + + //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution. + + //keep getting the next included time until it's farther than one second + // apart. At that point, lastDate is the last valid fire time. We return + // the second immediately following it. + while (difference == 1000) { + newDate = getTimeAfter(lastDate); + if (newDate == null){ + break; + } + + difference = newDate.getTime() - lastDate.getTime(); + + if (difference == 1000) { + lastDate = newDate; + } + } + + return new Date(lastDate.getTime() + 1000); + } + + /** + * Returns the time zone for which this CronExpression + * will be resolved. + */ + public TimeZone getTimeZone() { + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + + return timeZone; + } + + /** + * Sets the time zone for which this CronExpression + * will be resolved. + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Returns the string representation of the CronExpression + * + * @return a string representation of the CronExpression + */ + @Override + public String toString() { + return cronExpression; + } + + /** + * Indicates whether the specified cron expression can be parsed into a + * valid cron expression + * + * @param cronExpression the expression to evaluate + * @return a boolean indicating whether the given expression is a valid cron + * expression + */ + public static boolean isValidExpression(String cronExpression) { + + try { + new CronExpression(cronExpression); + } catch (ParseException pe) { + return false; + } + + return true; + } + + public static void validateExpression(String cronExpression) throws ParseException { + + new CronExpression(cronExpression); + } + + + //////////////////////////////////////////////////////////////////////////// + // + // Expression Parsing Functions + // + //////////////////////////////////////////////////////////////////////////// + + protected void buildExpression(String expression) throws ParseException { + expressionParsed = true; + + try { + + if (seconds == null) { + seconds = new TreeSet(); + } + if (minutes == null) { + minutes = new TreeSet(); + } + if (hours == null) { + hours = new TreeSet(); + } + if (daysOfMonth == null) { + daysOfMonth = new TreeSet(); + } + if (months == null) { + months = new TreeSet(); + } + if (daysOfWeek == null) { + daysOfWeek = new TreeSet(); + } + if (years == null) { + years = new TreeSet(); + } + + int exprOn = SECOND; + + StringTokenizer exprsTok = new StringTokenizer(expression, " \t", + false); + + while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { + String expr = exprsTok.nextToken().trim(); + + // throw an exception if L is used with other days of the month + if (exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); + } + // throw an exception if L is used with other days of the week + if (exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); + } + if (exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') + 1) != -1) { + throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); + } + + StringTokenizer vTok = new StringTokenizer(expr, ","); + while (vTok.hasMoreTokens()) { + String v = vTok.nextToken(); + storeExpressionVals(0, v, exprOn); + } + + exprOn++; + } + + if (exprOn <= DAY_OF_WEEK) { + throw new ParseException("Unexpected end of expression.", + expression.length()); + } + + if (exprOn <= YEAR) { + storeExpressionVals(0, "*", YEAR); + } + + TreeSet dow = getSet(DAY_OF_WEEK); + TreeSet dom = getSet(DAY_OF_MONTH); + + // Copying the logic from the UnsupportedOperationException below + boolean dayOfMSpec = !dom.contains(NO_SPEC); + boolean dayOfWSpec = !dow.contains(NO_SPEC); + + if (!dayOfMSpec || dayOfWSpec) { + if (!dayOfWSpec || dayOfMSpec) { + throw new ParseException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); + } + } + } catch (ParseException pe) { + throw pe; + } catch (Exception e) { + throw new ParseException("Illegal cron expression format (" + + e.toString() + ")", 0); + } + } + + protected int storeExpressionVals(int pos, String s, int type) + throws ParseException { + + int incr = 0; + int i = skipWhiteSpace(pos, s); + if (i >= s.length()) { + return i; + } + char c = s.charAt(i); + if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { + String sub = s.substring(i, i + 3); + int sval = -1; + int eval = -1; + if (type == MONTH) { + sval = getMonthNumber(sub) + 1; + if (sval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getMonthNumber(sub) + 1; + if (eval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + } + } + } else if (type == DAY_OF_WEEK) { + sval = getDayOfWeekNumber(sub); + if (sval < 0) { + throw new ParseException("Invalid Day-of-Week value: '" + + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getDayOfWeekNumber(sub); + if (eval < 0) { + throw new ParseException( + "Invalid Day-of-Week value: '" + sub + + "'", i); + } + } else if (c == '#') { + try { + i += 4; + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + } else if (c == 'L') { + lastdayOfWeek = true; + i++; + } + } + + } else { + throw new ParseException( + "Illegal characters for this position: '" + sub + "'", + i); + } + if (eval != -1) { + incr = 1; + } + addToSet(sval, eval, incr, type); + return (i + 3); + } + + if (c == '?') { + i++; + if ((i + 1) < s.length() + && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { + throw new ParseException("Illegal character after '?': " + + s.charAt(i), i); + } + if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { + throw new ParseException( + "'?' can only be specified for Day-of-Month or Day-of-Week.", + i); + } + if (type == DAY_OF_WEEK && !lastdayOfMonth) { + int val = daysOfMonth.last(); + if (val == NO_SPEC_INT) { + throw new ParseException( + "'?' can only be specified for Day-of-Month -OR- Day-of-Week.", + i); + } + } + + addToSet(NO_SPEC_INT, -1, 0, type); + return i; + } + + if (c == '*' || c == '/') { + if (c == '*' && (i + 1) >= s.length()) { + addToSet(ALL_SPEC_INT, -1, incr, type); + return i + 1; + } else if (c == '/' + && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s + .charAt(i + 1) == '\t')) { + throw new ParseException("'/' must be followed by an integer.", i); + } else if (c == '*') { + i++; + } + c = s.charAt(i); + if (c == '/') { // is an increment specified? + i++; + if (i >= s.length()) { + throw new ParseException("Unexpected end of string.", i); + } + + incr = getNumericValue(s, i); + + i++; + if (incr > 10) { + i++; + } + checkIncrementRange(incr, type, i); + } else { + incr = 1; + } + + addToSet(ALL_SPEC_INT, -1, incr, type); + return i; + } else if (c == 'L') { + i++; + if (type == DAY_OF_MONTH) { + lastdayOfMonth = true; + } + if (type == DAY_OF_WEEK) { + addToSet(7, 7, 0, type); + } + if (type == DAY_OF_MONTH && s.length() > i) { + c = s.charAt(i); + if (c == '-') { + ValueSet vs = getValue(0, s, i + 1); + lastdayOffset = vs.value; + if (lastdayOffset > 30) { + throw new ParseException("Offset from last day must be <= 30", i + 1); + } + i = vs.pos; + } + if (s.length() > i) { + c = s.charAt(i); + if (c == 'W') { + nearestWeekday = true; + i++; + } + } + } + return i; + } else if (c >= '0' && c <= '9') { + int val = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, -1, -1, type); + } else { + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(val, s, i); + val = vs.value; + i = vs.pos; + } + i = checkNext(i, s, val, type); + return i; + } + } else { + throw new ParseException("Unexpected character: " + c, i); + } + + return i; + } + + private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException { + if (incr > 59 && (type == SECOND || type == MINUTE)) { + throw new ParseException("Increment > 60 : " + incr, idxPos); + } else if (incr > 23 && (type == HOUR)) { + throw new ParseException("Increment > 24 : " + incr, idxPos); + } else if (incr > 31 && (type == DAY_OF_MONTH)) { + throw new ParseException("Increment > 31 : " + incr, idxPos); + } else if (incr > 7 && (type == DAY_OF_WEEK)) { + throw new ParseException("Increment > 7 : " + incr, idxPos); + } else if (incr > 12 && (type == MONTH)) { + throw new ParseException("Increment > 12 : " + incr, idxPos); + } + } + + protected int checkNext(int pos, String s, int val, int type) + throws ParseException { + + int end = -1; + int i = pos; + + if (i >= s.length()) { + addToSet(val, end, -1, type); + return i; + } + + char c = s.charAt(pos); + + if (c == 'L') { + if (type == DAY_OF_WEEK) { + if (val < 1 || val > 7) { + throw new ParseException("Day-of-Week values must be between 1 and 7", -1); + } + lastdayOfWeek = true; + } else { + throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); + } + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == 'W') { + if (type == DAY_OF_MONTH) { + nearestWeekday = true; + } else { + throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); + } + if (val > 31) { + throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); + } + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '#') { + if (type != DAY_OF_WEEK) { + throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); + } + i++; + try { + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '-') { + i++; + c = s.charAt(i); + int v = Integer.parseInt(String.valueOf(c)); + end = v; + i++; + if (i >= s.length()) { + addToSet(val, end, 1, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v, s, i); + end = vs.value; + i = vs.pos; + } + if (i < s.length() && ((c = s.charAt(i)) == '/')) { + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + addToSet(val, end, v2, type); + return i; + } + } else { + addToSet(val, end, 1, type); + return i; + } + } + + if (c == '/') { + if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') { + throw new ParseException("'/' must be followed by an integer.", i); + } + + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + checkIncrementRange(v2, type, i); + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + checkIncrementRange(v3, type, i); + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + throw new ParseException("Unexpected character '" + c + "' after '/'", i); + } + } + + addToSet(val, end, 0, type); + i++; + return i; + } + + public String getCronExpression() { + return cronExpression; + } + + public String getExpressionSummary() { + StringBuilder buf = new StringBuilder(); + + buf.append("seconds: "); + buf.append(getExpressionSetSummary(seconds)); + buf.append("\n"); + buf.append("minutes: "); + buf.append(getExpressionSetSummary(minutes)); + buf.append("\n"); + buf.append("hours: "); + buf.append(getExpressionSetSummary(hours)); + buf.append("\n"); + buf.append("daysOfMonth: "); + buf.append(getExpressionSetSummary(daysOfMonth)); + buf.append("\n"); + buf.append("months: "); + buf.append(getExpressionSetSummary(months)); + buf.append("\n"); + buf.append("daysOfWeek: "); + buf.append(getExpressionSetSummary(daysOfWeek)); + buf.append("\n"); + buf.append("lastdayOfWeek: "); + buf.append(lastdayOfWeek); + buf.append("\n"); + buf.append("nearestWeekday: "); + buf.append(nearestWeekday); + buf.append("\n"); + buf.append("NthDayOfWeek: "); + buf.append(nthdayOfWeek); + buf.append("\n"); + buf.append("lastdayOfMonth: "); + buf.append(lastdayOfMonth); + buf.append("\n"); + buf.append("years: "); + buf.append(getExpressionSetSummary(years)); + buf.append("\n"); + + return buf.toString(); + } + + protected String getExpressionSetSummary(Set set) { + + if (set.contains(NO_SPEC)) { + return "?"; + } + if (set.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator itr = set.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + protected String getExpressionSetSummary(ArrayList list) { + + if (list.contains(NO_SPEC)) { + return "?"; + } + if (list.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator itr = list.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + protected int skipWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) { + ; + } + + return i; + } + + protected int findNextWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) { + ; + } + + return i; + } + + protected void addToSet(int val, int end, int incr, int type) + throws ParseException { + + TreeSet set = getSet(type); + + if (type == SECOND || type == MINUTE) { + if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Minute and Second values must be between 0 and 59", + -1); + } + } else if (type == HOUR) { + if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Hour values must be between 0 and 23", -1); + } + } else if (type == DAY_OF_MONTH) { + if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day of month values must be between 1 and 31", -1); + } + } else if (type == MONTH) { + if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Month values must be between 1 and 12", -1); + } + } else if (type == DAY_OF_WEEK) { + if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day-of-Week values must be between 1 and 7", -1); + } + } + + if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { + if (val != -1) { + set.add(val); + } else { + set.add(NO_SPEC); + } + + return; + } + + int startAt = val; + int stopAt = end; + + if (val == ALL_SPEC_INT && incr <= 0) { + incr = 1; + set.add(ALL_SPEC); // put in a marker, but also fill values + } + + if (type == SECOND || type == MINUTE) { + if (stopAt == -1) { + stopAt = 59; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == HOUR) { + if (stopAt == -1) { + stopAt = 23; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == DAY_OF_MONTH) { + if (stopAt == -1) { + stopAt = 31; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == MONTH) { + if (stopAt == -1) { + stopAt = 12; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == DAY_OF_WEEK) { + if (stopAt == -1) { + stopAt = 7; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == YEAR) { + if (stopAt == -1) { + stopAt = MAX_YEAR; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1970; + } + } + + // if the end of the range is before the start, then we need to overflow into + // the next day, month etc. This is done by adding the maximum amount for that + // type, and using modulus max to determine the value being added. + int max = -1; + if (stopAt < startAt) { + switch (type) { + case SECOND: + max = 60; + break; + case MINUTE: + max = 60; + break; + case HOUR: + max = 24; + break; + case MONTH: + max = 12; + break; + case DAY_OF_WEEK: + max = 7; + break; + case DAY_OF_MONTH: + max = 31; + break; + case YEAR: + throw new IllegalArgumentException("Start year must be less than stop year"); + default: + throw new IllegalArgumentException("Unexpected type encountered"); + } + stopAt += max; + } + + for (int i = startAt; i <= stopAt; i += incr) { + if (max == -1) { + // ie: there's no max to overflow over + set.add(i); + } else { + // take the modulus to get the real value + int i2 = i % max; + + // 1-indexed ranges should not include 0, and should include their max + if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH)) { + i2 = max; + } + + set.add(i2); + } + } + } + + TreeSet getSet(int type) { + switch (type) { + case SECOND: + return seconds; + case MINUTE: + return minutes; + case HOUR: + return hours; + case DAY_OF_MONTH: + return daysOfMonth; + case MONTH: + return months; + case DAY_OF_WEEK: + return daysOfWeek; + case YEAR: + return years; + default: + return null; + } + } + + protected ValueSet getValue(int v, String s, int i) { + char c = s.charAt(i); + StringBuilder s1 = new StringBuilder(String.valueOf(v)); + while (c >= '0' && c <= '9') { + s1.append(c); + i++; + if (i >= s.length()) { + break; + } + c = s.charAt(i); + } + ValueSet val = new ValueSet(); + + val.pos = (i < s.length()) ? i : i + 1; + val.value = Integer.parseInt(s1.toString()); + return val; + } + + protected int getNumericValue(String s, int i) { + int endOfVal = findNextWhiteSpace(i, s); + String val = s.substring(i, endOfVal); + return Integer.parseInt(val); + } + + protected int getMonthNumber(String s) { + Integer integer = monthMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + protected int getDayOfWeekNumber(String s) { + Integer integer = dayMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Computation Functions + // + //////////////////////////////////////////////////////////////////////////// + + public Date getTimeAfter(Date afterTime) { + + // Computation is based on Gregorian year only. + Calendar cl = new GregorianCalendar(getTimeZone()); + + // move ahead one second, since we're computing the time *after* the + // given time + afterTime = new Date(afterTime.getTime() + 1000); + // CronTrigger does not deal with milliseconds + cl.setTime(afterTime); + cl.set(Calendar.MILLISECOND, 0); + + boolean gotOne = false; + // loop until we've computed the next time, or we've past the endTime + while (!gotOne) { + + //if (endTime != null && cl.getTime().after(endTime)) return null; + if (cl.get(Calendar.YEAR) > 2999) { // prevent endless loop... + return null; + } + + SortedSet st = null; + int t = 0; + + int sec = cl.get(Calendar.SECOND); + int min = cl.get(Calendar.MINUTE); + + // get second................................................. + st = seconds.tailSet(sec); + if (st != null && st.size() != 0) { + sec = st.first(); + } else { + sec = seconds.first(); + min++; + cl.set(Calendar.MINUTE, min); + } + cl.set(Calendar.SECOND, sec); + + min = cl.get(Calendar.MINUTE); + int hr = cl.get(Calendar.HOUR_OF_DAY); + t = -1; + + // get minute................................................. + st = minutes.tailSet(min); + if (st != null && st.size() != 0) { + t = min; + min = st.first(); + } else { + min = minutes.first(); + hr++; + } + if (min != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, min); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.MINUTE, min); + + hr = cl.get(Calendar.HOUR_OF_DAY); + int day = cl.get(Calendar.DAY_OF_MONTH); + t = -1; + + // get hour................................................... + st = hours.tailSet(hr); + if (st != null && st.size() != 0) { + t = hr; + hr = st.first(); + } else { + hr = hours.first(); + day++; + } + if (hr != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.HOUR_OF_DAY, hr); + + day = cl.get(Calendar.DAY_OF_MONTH); + int mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + t = -1; + int tmon = mon; + + // get day................................................... + boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); + boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); + if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule + st = daysOfMonth.tailSet(day); + if (lastdayOfMonth) { + if (!nearestWeekday) { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + if (t > day) { + mon++; + if (mon > 12) { + mon = 1; + tmon = 3333; // ensure test of mon != tmon further below fails + cl.add(Calendar.YEAR, 1); + } + day = 1; + } + } else { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + + Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if (dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if (dow == Calendar.SATURDAY) { + day -= 1; + } else if (dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if (dow == Calendar.SUNDAY) { + day += 1; + } + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if (nTime.before(afterTime)) { + day = 1; + mon++; + } + } + } else if (nearestWeekday) { + t = day; + day = daysOfMonth.first(); + + Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if (dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if (dow == Calendar.SATURDAY) { + day -= 1; + } else if (dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if (dow == Calendar.SUNDAY) { + day += 1; + } + + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if (nTime.before(afterTime)) { + day = daysOfMonth.first(); + mon++; + } + } else if (st != null && st.size() != 0) { + t = day; + day = st.first(); + // make sure we don't over-run a short month, such as february + int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + if (day > lastDay) { + day = daysOfMonth.first(); + mon++; + } + } else { + day = daysOfMonth.first(); + mon++; + } + + if (day != t || mon != tmon) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we + // are 1-based + continue; + } + } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule + if (lastdayOfWeek) { // are we looking for the last XXX day of + // the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // did we already miss the + // last one? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + + // find date of last occurrence of this day in this month... + while ((day + daysToAdd + 7) <= lDay) { + daysToAdd += 7; + } + + day += daysToAdd; + + if (daysToAdd > 0) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are not promoting the month + continue; + } + + } else if (nthdayOfWeek != 0) { + // are we looking for the Nth XXX day in the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } else if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + boolean dayShifted = false; + if (daysToAdd > 0) { + dayShifted = true; + } + + day += daysToAdd; + int weekOfMonth = day / 7; + if (day % 7 > 0) { + weekOfMonth++; + } + + daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; + day += daysToAdd; + if (daysToAdd < 0 + || day > getLastDayOfMonth(mon, cl + .get(Calendar.YEAR))) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0 || dayShifted) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are NOT promoting the month + continue; + } + } else { + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int dow = daysOfWeek.first(); // desired + // d-o-w + st = daysOfWeek.tailSet(cDow); + if (st != null && st.size() > 0) { + dow = st.first(); + } + + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // will we pass the end of + // the month? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0) { // are we swithing days? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, + // and we are 1-based + continue; + } + } + } else { // dayOfWSpec && !dayOfMSpec + throw new UnsupportedOperationException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); + } + cl.set(Calendar.DAY_OF_MONTH, day); + + mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + int year = cl.get(Calendar.YEAR); + t = -1; + + // test for expressions that never generate a valid fire date, + // but keep looping... + if (year > MAX_YEAR) { + return null; + } + + // get month................................................... + st = months.tailSet(mon); + if (st != null && st.size() != 0) { + t = mon; + mon = st.first(); + } else { + mon = months.first(); + year++; + } + if (mon != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + + year = cl.get(Calendar.YEAR); + t = -1; + + // get year................................................... + st = years.tailSet(year); + if (st != null && st.size() != 0) { + t = year; + year = st.first(); + } else { + return null; // ran out of years... + } + + if (year != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, 0); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.YEAR, year); + + gotOne = true; + } // while( !done ) + + return cl.getTime(); + } + + /** + * Advance the calendar to the particular hour paying particular attention + * to daylight saving problems. + * + * @param cal the calendar to operate on + * @param hour the hour to set + */ + protected void setCalendarHour(Calendar cal, int hour) { + cal.set(Calendar.HOUR_OF_DAY, hour); + if (cal.get(Calendar.HOUR_OF_DAY) != hour && hour != 24) { + cal.set(Calendar.HOUR_OF_DAY, hour + 1); + } + } + + /** + * NOT YET IMPLEMENTED: Returns the time before the given time + * that the CronExpression matches. + */ + public Date getTimeBefore(Date endTime) { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + /** + * NOT YET IMPLEMENTED: Returns the final time that the + * CronExpression will match. + */ + public Date getFinalFireTime() { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + protected boolean isLeapYear(int year) { + return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); + } + + protected int getLastDayOfMonth(int monthNum, int year) { + + switch (monthNum) { + case 1: + return 31; + case 2: + return (isLeapYear(year)) ? 29 : 28; + case 3: + return 31; + case 4: + return 30; + case 5: + return 31; + case 6: + return 30; + case 7: + return 31; + case 8: + return 31; + case 9: + return 30; + case 10: + return 31; + case 11: + return 30; + case 12: + return 31; + default: + throw new IllegalArgumentException("Illegal month number: " + + monthNum); + } + } + + + private void readObject(java.io.ObjectInputStream stream) + throws java.io.IOException, ClassNotFoundException { + + stream.defaultReadObject(); + try { + buildExpression(cronExpression); + } catch (Exception ignore) { + } // never happens + } + + @Override + @Deprecated + public Object clone() { + return new CronExpression(this); + } +} + +class ValueSet { + public int value; + + public int pos; +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/impl/JobmanClientImpl.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/impl/JobmanClientImpl.java new file mode 100644 index 0000000..3d25dcf --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/impl/JobmanClientImpl.java @@ -0,0 +1,272 @@ +package com.sonic.common.client.impl; + +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.sonic.common.AppRuntime; +import com.sonic.common.client.JobmanClient; +import com.sonic.common.enums.AppEnv; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.core.task.TaskExecutor; +import org.springframework.data.redis.core.RedisTemplate; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.sql.Date; +import java.text.ParseException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +@Slf4j +public class JobmanClientImpl implements JobmanClient, DisposableBean { + private static final int MAX_LOGS_SIZE = 1_000; + + @Setter(AccessLevel.PROTECTED) + private String hostIp; + @Setter(AccessLevel.PROTECTED) + private TaskExecutor executor; + @Setter(AccessLevel.PROTECTED) + private AppRuntime appRuntime; + @Setter(AccessLevel.PROTECTED) + private RedisTemplate redisTemplate; + @Setter(AccessLevel.PROTECTED) + private JobmanClient.LockPolicy globalLockPolicy; + + /** + * jobName对应的status. 用于加锁保持run与destroy互斥 + */ + private final Map statusMap = Maps.newConcurrentMap(); + + @Override + public void run(String jobName, LockPolicy lockPolicy, Function runner) { + Objects.requireNonNull(lockPolicy); + + //只在第一次没有的jobName对应的Status情况下才build新的Context + statusMap.putIfAbsent(jobName, JobStatus.buildContext(jobName, lockPolicy)); + JobStatus status = statusMap.get(jobName); + synchronized (status) { + if (status.isDestroyed() || !lockPolicy.tryLock()) { + return; + } + + JobContext jobContext = JobContext.builder() + .jobName(jobName) + .startTime(LocalDateTime.now()) + .build(); + JobResult jobResult; + try { + jobResult = runner.apply(jobContext); + } catch (Exception ex) { + jobResult = JobResult.fail(); + jobContext.log("=== run job error ===, name = {}, appRuntime = {}, context = {}", + jobName, appRuntime.toJson().toJSONString(), JSONObject.toJSONString(jobContext), ex); + } + + jobContext.setEndTime(LocalDateTime.now()); + JobResult finalJobResult = jobResult; + executor.execute(() -> { + Map params = ImmutableMap.builder() + .put("jobName", jobContext.getJobName()) + .put("status", finalJobResult.getJobStatus()) + .put("startTime", jobContext.getStartTime()) + .put("endTime", jobContext.getEndTime()) + .put("ip", Optional.ofNullable(hostIp).orElse(StringUtils.EMPTY)) + .put("loadCount", finalJobResult.getLoadCount()) + .put("logs", jobContext.getLogs().subList(0, Math.min(MAX_LOGS_SIZE, jobContext.getLogs().size()))) + .put("ext", Optional.ofNullable(finalJobResult.getExt()).orElse(ImmutableMap.of())) + .build(); + log.info(JSONObject.toJSONString(params)); + }); + } + } + + @Override + public void run(String jobName, Long lockDurationSeconds, Function runner) { + LockPolicy lockPolicy = Optional.ofNullable(globalLockPolicy) + .orElse(RedisLockPolicy.builder() + .appRuntime(appRuntime) + .redisTemplate(redisTemplate) + .lockDurationSeconds(lockDurationSeconds) + .jobName(jobName) + .build()); + run(jobName, lockPolicy, runner); + } + + @Override + public boolean isCronSatisfied(String cornExpression, LocalDateTime dateTime) { + CronExpression expression; + try { + expression = new CronExpression(cornExpression); + } catch (ParseException e) { + throw new RuntimeException(String.format("invalid corn expression, expression = %s", cornExpression), e); + } + return expression.isSatisfiedBy(Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant())); + } + + @Override + public void destroy() { + //由于destroy方法有jobStatus的锁. 因此并行调用. 该方法只会由容器调用一次. 不需要考虑性能 + statusMap.forEach((key, value) -> value.destroy()); + } + + public static class RedisLockPolicy implements LockPolicy { + private final String jobName; + private final long lockDurationSeconds; + private final AppRuntime appRuntime; + private final RedisTemplate redisTemplate; + + @lombok.Builder + public RedisLockPolicy(String jobName, long lockDurationSeconds, AppRuntime appRuntime, RedisTemplate redisTemplate) { + this.jobName = jobName; + this.lockDurationSeconds = lockDurationSeconds; + this.appRuntime = appRuntime; + this.redisTemplate = redisTemplate; + } + + @Override + public boolean tryLock() { + String lockKey = buildKey(); + redisTemplate.opsForValue().setIfAbsent(lockKey, appRuntime.getInstanceId(), lockDurationSeconds, TimeUnit.SECONDS); + + String locker = redisTemplate.opsForValue().get(lockKey); + return appRuntime.getInstanceId().equals(locker); + } + + @Override + public void destroy() { + String redisKey = buildKey(); + //如果销毁时是本节点锁定, 则删除锁 + String locker = redisTemplate.opsForValue().get(redisKey); + if (appRuntime.getInstanceId().equals(locker)) { + redisTemplate.delete(redisKey); + } + } + + private String buildKey() { + return appRuntime.buildPrefixKey("jobman", jobName, "_locker_"); + } + } + + @AllArgsConstructor + private static class JobStatus { + private final String jobName; + private final LockPolicy lockPolicy; + private boolean isAlive; + + public void destroy() { + synchronized (this) { + isAlive = false; + log.info("destroy jobman, destroy by container, jobName = {}, lockPolicy = {}", jobName, lockPolicy.getClass().getName()); + lockPolicy.destroy(); + } + } + + public boolean isDestroyed() { + return !isAlive; + } + + public static JobStatus buildContext(String jobName, LockPolicy lockPolicy) { + return new JobStatus(jobName, lockPolicy, true); + } + } + + @Data + public static class Builder { + private TaskExecutor taskExecutor; + private AppRuntime appRuntime; + private RedisTemplate redisTemplate; + private LockPolicy lockPolicy; + + public Builder taskExecutor(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + return this; + } + + public Builder appRuntime(AppRuntime appRuntime) { + this.appRuntime = appRuntime; + return this; + } + + public Builder redisTemplate(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + return this; + } + + public Builder lockPolicy(LockPolicy lockPolicy) { + this.lockPolicy = lockPolicy; + return this; + } + + protected JobmanClient buildService() { + return new JobmanClientImpl(); + } + + public JobmanClient build() { + //XXX local环境避免调试等待. 所以直接运行runner + if (appRuntime.getEnv() == AppEnv.local) { + return new JobmanClient() { + @Override + public void run(String jobName, LockPolicy lockPolicy, Function runner) { + JobContext jobContext = JobContext.builder() + .jobName(jobName) + .startTime(LocalDateTime.now()) + .build(); + JobResult result = runner.apply(jobContext); + jobContext.setEndTime(LocalDateTime.now()); + log.info("job client execute, context = {}, result = {}", jobContext, result); + } + + @Override + public void run(String jobName, Long lockDurationSeconds, Function runner) { + run(jobName, () -> true, runner); + } + + @Override + public boolean isCronSatisfied(String cornExpression, LocalDateTime dateTime) { + CronExpression expression; + try { + expression = new CronExpression(cornExpression); + } catch (ParseException e) { + throw new RuntimeException(String.format("invalid corn expression, expression = %s", cornExpression), e); + } + return expression.isSatisfiedBy(Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant())); + } + }; + } + + Objects.requireNonNull(redisTemplate); + Objects.requireNonNull(taskExecutor); + + JobmanClientImpl client = (JobmanClientImpl) buildService(); + client.setExecutor(taskExecutor); + client.setRedisTemplate(redisTemplate); + + client.setAppRuntime(appRuntime); + client.setGlobalLockPolicy(lockPolicy); + client.setHostIp(getHostIp()); + + return client; + } + + private String getHostIp() { + String hostIP = "unknown"; + try { + hostIP = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("failed to get host ip"); + } + return hostIP; + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/util/ParameterFormatter.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/util/ParameterFormatter.java new file mode 100644 index 0000000..2a95669 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/client/util/ParameterFormatter.java @@ -0,0 +1,641 @@ +package com.gamers.bs.client.util; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 根据参数格式化message + * {@link org.apache.logging.log4j.message} + */ +public class ParameterFormatter { + /** + * Prefix for recursion. + */ + static final String RECURSION_PREFIX = "[..."; + /** + * Suffix for recursion. + */ + static final String RECURSION_SUFFIX = "...]"; + + /** + * Prefix for errors. + */ + static final String ERROR_PREFIX = "[!!!"; + /** + * Separator for errors. + */ + static final String ERROR_SEPARATOR = "=>"; + /** + * Separator for error messages. + */ + static final String ERROR_MSG_SEPARATOR = ":"; + /** + * Suffix for errors. + */ + static final String ERROR_SUFFIX = "!!!]"; + + private static final char DELIM_START = '{'; + private static final char DELIM_STOP = '}'; + private static final char ESCAPE_CHAR = '\\'; + + private static ThreadLocal threadLocalSimpleDateFormat = new ThreadLocal<>(); + + private ParameterFormatter() { + } + + /** + * Counts the number of unescaped placeholders in the given messagePattern. + * + * @param messagePattern the message pattern to be analyzed. + * @return the number of unescaped placeholders. + */ + static int countArgumentPlaceholders(final String messagePattern) { + if (messagePattern == null) { + return 0; + } + final int length = messagePattern.length(); + int result = 0; + boolean isEscaped = false; + for (int i = 0; i < length - 1; i++) { + final char curChar = messagePattern.charAt(i); + if (curChar == ESCAPE_CHAR) { + isEscaped = !isEscaped; + } else if (curChar == DELIM_START) { + if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) { + result++; + i++; + } + isEscaped = false; + } else { + isEscaped = false; + } + } + return result; + } + + /** + * Counts the number of unescaped placeholders in the given messagePattern. + * + * @param messagePattern the message pattern to be analyzed. + * @return the number of unescaped placeholders. + */ + static int countArgumentPlaceholders2(final String messagePattern, final int[] indices) { + if (messagePattern == null) { + return 0; + } + final int length = messagePattern.length(); + int result = 0; + boolean isEscaped = false; + for (int i = 0; i < length - 1; i++) { + final char curChar = messagePattern.charAt(i); + if (curChar == ESCAPE_CHAR) { + isEscaped = !isEscaped; + indices[0] = -1; // escaping means fast path is not available... + result++; + } else if (curChar == DELIM_START) { + if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) { + indices[result] = i; + result++; + i++; + } + isEscaped = false; + } else { + isEscaped = false; + } + } + return result; + } + + /** + * Counts the number of unescaped placeholders in the given messagePattern. + * + * @param messagePattern the message pattern to be analyzed. + * @return the number of unescaped placeholders. + */ + static int countArgumentPlaceholders3(final char[] messagePattern, final int length, final int[] indices) { + int result = 0; + boolean isEscaped = false; + for (int i = 0; i < length - 1; i++) { + final char curChar = messagePattern[i]; + if (curChar == ESCAPE_CHAR) { + isEscaped = !isEscaped; + } else if (curChar == DELIM_START) { + if (!isEscaped && messagePattern[i + 1] == DELIM_STOP) { + indices[result] = i; + result++; + i++; + } + isEscaped = false; + } else { + isEscaped = false; + } + } + return result; + } + + /** + * Replace placeholders in the given messagePattern with arguments. + * + * @param messagePattern the message pattern containing placeholders. + * @param arguments the arguments to be used to replace placeholders. + * @return the formatted message. + */ + public static String format(final String messagePattern, final Object[] arguments) { + final StringBuilder result = new StringBuilder(); + final int argCount = arguments == null ? 0 : arguments.length; + formatMessage(result, messagePattern, arguments, argCount); + return result.toString(); + } + + /** + * Replace placeholders in the given messagePattern with arguments. + * + * @param buffer the buffer to write the formatted message into + * @param messagePattern the message pattern containing placeholders. + * @param arguments the arguments to be used to replace placeholders. + */ + static void formatMessage2(final StringBuilder buffer, final String messagePattern, + final Object[] arguments, final int argCount, final int[] indices) { + if (messagePattern == null || arguments == null || argCount == 0) { + buffer.append(messagePattern); + return; + } + int previous = 0; + for (int i = 0; i < argCount; i++) { + buffer.append(messagePattern, previous, indices[i]); + previous = indices[i] + 2; + recursiveDeepToString(arguments[i], buffer, null); + } + buffer.append(messagePattern, previous, messagePattern.length()); + } + + /** + * Replace placeholders in the given messagePattern with arguments. + * + * @param buffer the buffer to write the formatted message into + * @param messagePattern the message pattern containing placeholders. + * @param arguments the arguments to be used to replace placeholders. + */ + static void formatMessage3(final StringBuilder buffer, final char[] messagePattern, final int patternLength, + final Object[] arguments, final int argCount, final int[] indices) { + if (messagePattern == null) { + return; + } + if (arguments == null || argCount == 0) { + buffer.append(messagePattern); + return; + } + int previous = 0; + for (int i = 0; i < argCount; i++) { + buffer.append(messagePattern, previous, indices[i]); + previous = indices[i] + 2; + recursiveDeepToString(arguments[i], buffer, null); + } + buffer.append(messagePattern, previous, patternLength); + } + + /** + * Replace placeholders in the given messagePattern with arguments. + * + * @param buffer the buffer to write the formatted message into + * @param messagePattern the message pattern containing placeholders. + * @param arguments the arguments to be used to replace placeholders. + */ + static void formatMessage(final StringBuilder buffer, final String messagePattern, + final Object[] arguments, final int argCount) { + if (messagePattern == null || arguments == null || argCount == 0) { + buffer.append(messagePattern); + return; + } + int escapeCounter = 0; + int currentArgument = 0; + int i = 0; + final int len = messagePattern.length(); + for (; i < len - 1; i++) { // last char is excluded from the loop + final char curChar = messagePattern.charAt(i); + if (curChar == ESCAPE_CHAR) { + escapeCounter++; + } else { + if (isDelimPair(curChar, messagePattern, i)) { // looks ahead one char + i++; + + // write escaped escape chars + writeEscapedEscapeChars(escapeCounter, buffer); + + if (isOdd(escapeCounter)) { + // i.e. escaped: write escaped escape chars + writeDelimPair(buffer); + } else { + // unescaped + writeArgOrDelimPair(arguments, argCount, currentArgument, buffer); + currentArgument++; + } + } else { + handleLiteralChar(buffer, escapeCounter, curChar); + } + escapeCounter = 0; + } + } + handleRemainingCharIfAny(messagePattern, len, buffer, escapeCounter, i); + } + + /** + * Returns {@code true} if the specified char and the char at {@code curCharIndex + 1} in the specified message + * pattern together form a "{}" delimiter pair, returns {@code false} otherwise. + */ + // Profiling showed this method is important to log4j performance. Modify with care! + // 22 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 + private static boolean isDelimPair(final char curChar, final String messagePattern, final int curCharIndex) { + return curChar == DELIM_START && messagePattern.charAt(curCharIndex + 1) == DELIM_STOP; + } + + /** + * Detects whether the message pattern has been fully processed or if an unprocessed character remains and processes + * it if necessary, returning the resulting position in the result char array. + */ + // Profiling showed this method is important to log4j performance. Modify with care! + // 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 + private static void handleRemainingCharIfAny(final String messagePattern, final int len, + final StringBuilder buffer, final int escapeCounter, final int i) { + if (i == len - 1) { + final char curChar = messagePattern.charAt(i); + handleLastChar(buffer, escapeCounter, curChar); + } + } + + /** + * Processes the last unprocessed character and returns the resulting position in the result char array. + */ + // Profiling showed this method is important to log4j performance. Modify with care! + // 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 + private static void handleLastChar(final StringBuilder buffer, final int escapeCounter, final char curChar) { + if (curChar == ESCAPE_CHAR) { + writeUnescapedEscapeChars(escapeCounter + 1, buffer); + } else { + handleLiteralChar(buffer, escapeCounter, curChar); + } + } + + /** + * Processes a literal char (neither an '\' escape char nor a "{}" delimiter pair) and returns the resulting + * position. + */ + // Profiling showed this method is important to log4j performance. Modify with care! + // 16 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 + private static void handleLiteralChar(final StringBuilder buffer, final int escapeCounter, final char curChar) { + // any other char beside ESCAPE or DELIM_START/STOP-combo + // write unescaped escape chars + writeUnescapedEscapeChars(escapeCounter, buffer); + buffer.append(curChar); + } + + /** + * Writes "{}" to the specified result array at the specified position and returns the resulting position. + */ + // Profiling showed this method is important to log4j performance. Modify with care! + // 18 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 + private static void writeDelimPair(final StringBuilder buffer) { + buffer.append(DELIM_START); + buffer.append(DELIM_STOP); + } + + /** + * Returns {@code true} if the specified parameter is odd. + */ + // Profiling showed this method is important to log4j performance. Modify with care! + // 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 + private static boolean isOdd(final int number) { + return (number & 1) == 1; + } + + /** + * Writes a '\' char to the specified result array (starting at the specified position) for each pair of + * '\' escape chars encountered in the message format and returns the resulting position. + */ + // Profiling showed this method is important to log4j performance. Modify with care! + // 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 + private static void writeEscapedEscapeChars(final int escapeCounter, final StringBuilder buffer) { + final int escapedEscapes = escapeCounter >> 1; // divide by two + writeUnescapedEscapeChars(escapedEscapes, buffer); + } + + /** + * Writes the specified number of '\' chars to the specified result array (starting at the specified position) and + * returns the resulting position. + */ + // Profiling showed this method is important to log4j performance. Modify with care! + // 20 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 + private static void writeUnescapedEscapeChars(int escapeCounter, final StringBuilder buffer) { + while (escapeCounter > 0) { + buffer.append(ESCAPE_CHAR); + escapeCounter--; + } + } + + /** + * Appends the argument at the specified argument index (or, if no such argument exists, the "{}" delimiter pair) to + * the specified result char array at the specified position and returns the resulting position. + */ + // Profiling showed this method is important to log4j performance. Modify with care! + // 25 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 + private static void writeArgOrDelimPair(final Object[] arguments, final int argCount, final int currentArgument, + final StringBuilder buffer) { + if (currentArgument < argCount) { + recursiveDeepToString(arguments[currentArgument], buffer, null); + } else { + writeDelimPair(buffer); + } + } + + /** + * This method performs a deep toString of the given Object. + * Primitive arrays are converted using their respective Arrays.toString methods while + * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could + * contain themselves. + *

+ * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a + * behavior. They only check if the container is directly contained in itself, but not if a contained container + * contains the original one. Because of that, Arrays.toString(Object[]) isn't safe either. + * Confusing? Just read the last paragraph again and check the respective toString() implementation. + *

+ *

+ * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o) + * would produce a relatively hard-to-debug StackOverflowError. + *

+ * + * @param o The object. + * @return The String representation. + */ + static String deepToString(final Object o) { + if (o == null) { + return null; + } + if (o instanceof String) { + return (String) o; + } + final StringBuilder str = new StringBuilder(); + final Set dejaVu = new HashSet<>(); // that's actually a neat name ;) + recursiveDeepToString(o, str, dejaVu); + return str.toString(); + } + + /** + * This method performs a deep toString of the given Object. + * Primitive arrays are converted using their respective Arrays.toString methods while + * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could + * contain themselves. + *

+ * dejaVu is used in case of those container types to prevent an endless recursion. + *

+ *

+ * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a + * behavior. + * They only check if the container is directly contained in itself, but not if a contained container contains the + * original one. Because of that, Arrays.toString(Object[]) isn't safe either. + * Confusing? Just read the last paragraph again and check the respective toString() implementation. + *

+ *

+ * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o) + * would produce a relatively hard-to-debug StackOverflowError. + *

+ * + * @param o the Object to convert into a String + * @param str the StringBuilder that o will be appended to + * @param dejaVu a list of container identities that were already used. + */ + private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set dejaVu) { + if (appendSpecialTypes(o, str)) { + return; + } + if (isMaybeRecursive(o)) { + appendPotentiallyRecursiveValue(o, str, dejaVu); + } else { + tryObjectToString(o, str); + } + } + + private static boolean appendSpecialTypes(final Object o, final StringBuilder str) { + if (o == null || o instanceof String) { + str.append((String) o); + return true; + } else if (o instanceof CharSequence) { + str.append((CharSequence) o); + return true; + } else if (o instanceof Integer) { + str.append(((Integer) o).intValue()); + return true; + } else if (o instanceof Long) { + str.append(((Long) o).longValue()); + return true; + } else if (o instanceof Double) { + str.append(((Double) o).doubleValue()); + return true; + } else if (o instanceof Boolean) { + str.append(((Boolean) o).booleanValue()); + return true; + } else if (o instanceof Character) { + str.append(((Character) o).charValue()); + return true; + } else if (o instanceof Short) { + str.append(((Short) o).shortValue()); + return true; + } else if (o instanceof Float) { + str.append(((Float) o).floatValue()); + return true; + } + return appendDate(o, str); + } + + private static boolean appendDate(final Object o, final StringBuilder str) { + if (!(o instanceof Date)) { + return false; + } + final Date date = (Date) o; + final SimpleDateFormat format = getSimpleDateFormat(); + str.append(format.format(date)); + return true; + } + + private static SimpleDateFormat getSimpleDateFormat() { + SimpleDateFormat result = threadLocalSimpleDateFormat.get(); + if (result == null) { + result = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + threadLocalSimpleDateFormat.set(result); + } + return result; + } + + /** + * Returns {@code true} if the specified object is an array, a Map or a Collection. + */ + private static boolean isMaybeRecursive(final Object o) { + return o.getClass().isArray() || o instanceof Map || o instanceof Collection; + } + + private static void appendPotentiallyRecursiveValue(final Object o, final StringBuilder str, + final Set dejaVu) { + final Class oClass = o.getClass(); + if (oClass.isArray()) { + appendArray(o, str, dejaVu, oClass); + } else if (o instanceof Map) { + appendMap(o, str, dejaVu); + } else if (o instanceof Collection) { + appendCollection(o, str, dejaVu); + } + } + + private static void appendArray(final Object o, final StringBuilder str, Set dejaVu, + final Class oClass) { + if (oClass == byte[].class) { + str.append(Arrays.toString((byte[]) o)); + } else if (oClass == short[].class) { + str.append(Arrays.toString((short[]) o)); + } else if (oClass == int[].class) { + str.append(Arrays.toString((int[]) o)); + } else if (oClass == long[].class) { + str.append(Arrays.toString((long[]) o)); + } else if (oClass == float[].class) { + str.append(Arrays.toString((float[]) o)); + } else if (oClass == double[].class) { + str.append(Arrays.toString((double[]) o)); + } else if (oClass == boolean[].class) { + str.append(Arrays.toString((boolean[]) o)); + } else if (oClass == char[].class) { + str.append(Arrays.toString((char[]) o)); + } else { + if (dejaVu == null) { + dejaVu = new HashSet<>(); + } + // special handling of container Object[] + final String id = identityToString(o); + if (dejaVu.contains(id)) { + str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX); + } else { + dejaVu.add(id); + final Object[] oArray = (Object[]) o; + str.append('['); + boolean first = true; + for (final Object current : oArray) { + if (first) { + first = false; + } else { + str.append(", "); + } + recursiveDeepToString(current, str, new HashSet<>(dejaVu)); + } + str.append(']'); + } + //str.append(Arrays.deepToString((Object[]) o)); + } + } + + private static void appendMap(final Object o, final StringBuilder str, Set dejaVu) { + // special handling of container Map + if (dejaVu == null) { + dejaVu = new HashSet<>(); + } + final String id = identityToString(o); + if (dejaVu.contains(id)) { + str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX); + } else { + dejaVu.add(id); + final Map oMap = (Map) o; + str.append('{'); + boolean isFirst = true; + for (final Object o1 : oMap.entrySet()) { + final Map.Entry current = (Map.Entry) o1; + if (isFirst) { + isFirst = false; + } else { + str.append(", "); + } + final Object key = current.getKey(); + final Object value = current.getValue(); + recursiveDeepToString(key, str, new HashSet<>(dejaVu)); + str.append('='); + recursiveDeepToString(value, str, new HashSet<>(dejaVu)); + } + str.append('}'); + } + } + + private static void appendCollection(final Object o, final StringBuilder str, Set dejaVu) { + // special handling of container Collection + if (dejaVu == null) { + dejaVu = new HashSet<>(); + } + final String id = identityToString(o); + if (dejaVu.contains(id)) { + str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX); + } else { + dejaVu.add(id); + final Collection oCol = (Collection) o; + str.append('['); + boolean isFirst = true; + for (final Object anOCol : oCol) { + if (isFirst) { + isFirst = false; + } else { + str.append(", "); + } + recursiveDeepToString(anOCol, str, new HashSet<>(dejaVu)); + } + str.append(']'); + } + } + + private static void tryObjectToString(final Object o, final StringBuilder str) { + // it's just some other Object, we can only use toString(). + try { + str.append(o.toString()); + } catch (final Throwable t) { + handleErrorInObjectToString(o, str, t); + } + } + + private static void handleErrorInObjectToString(final Object o, final StringBuilder str, final Throwable t) { + str.append(ERROR_PREFIX); + str.append(identityToString(o)); + str.append(ERROR_SEPARATOR); + final String msg = t.getMessage(); + final String className = t.getClass().getName(); + str.append(className); + if (!className.equals(msg)) { + str.append(ERROR_MSG_SEPARATOR); + str.append(msg); + } + str.append(ERROR_SUFFIX); + } + + /** + * This method returns the same as if Object.toString() would not have been + * overridden in obj. + *

+ * Note that this isn't 100% secure as collisions can always happen with hash codes. + *

+ *

+ * Copied from Object.hashCode(): + *

+ *
+ * As much as is reasonably practical, the hashCode method defined by + * class {@code Object} does return distinct integers for distinct + * objects. (This is typically implemented by converting the internal + * address of the object into an integer, but this implementation + * technique is not required by the Java™ programming language.) + *
+ * + * @param obj the Object that is to be converted into an identity string. + * @return the identity string as also defined in Object.toString() + */ + static String identityToString(final Object obj) { + if (obj == null) { + return null; + } + return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj)); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/config/DefaultWebMvcConfig.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/config/DefaultWebMvcConfig.java new file mode 100644 index 0000000..504bf2f --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/config/DefaultWebMvcConfig.java @@ -0,0 +1,357 @@ +package com.sonic.common.config; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.AuthCallerResolver; +import com.sonic.common.auth.OriginalCallerResolver; +import com.sonic.common.auth.SessionResolver; +import com.sonic.common.context.HttpRequestContext; +import com.sonic.common.context.RequestContextInterceptor; +import com.sonic.common.exception.AbstractExceptionHandler; +import com.sonic.common.log.TraceInterceptor; +import com.sonic.common.utils.DateConvertUtils; +import com.sonic.common.utils.FastjsonUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.convert.converter.Converter; +import org.springframework.format.FormatterRegistry; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.validation.Validator; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.yaml.snakeyaml.Yaml; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.parser.ParserConfig; +import com.alibaba.fastjson.parser.deserializer.Jdk8DateCodec; +import com.alibaba.fastjson.serializer.SerializeConfig; +import com.alibaba.fastjson.support.config.FastJsonConfig; +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; + +import lombok.extern.slf4j.Slf4j; + +/** + * @author code + */ +@Slf4j +@Import({DefaultWebMvcConfig.GlobalExceptionHandler.class}) +public class DefaultWebMvcConfig extends DelegatingWebMvcConfiguration implements InitializingBean { + private static final String ACCESS_CONTROL_MAX_AGE = String.valueOf(TimeUnit.DAYS.toSeconds(1)); + @Autowired + private DispatcherServlet servlet; + + @Autowired + private AppRuntime appRuntime; + + // XXX:handlerInterceptors可以为空,不做非空约束 + @Autowired(required = false) + private HandlerInterceptor[] handlerInterceptors; + + @Autowired(required = false) + private HandlerMethodArgumentResolver[] handlerMethodArgumentResolvers; + + //该validator由ValidationAutoConfiguration提供. + @Autowired(required = false) + @Qualifier(value = "defaultValidator") + Validator validator; + + /** 系统使用的时区 (默认为 CTT 中国上海时区)*/ + @Value("${shortZoneId.system:CTT}") + private String systemShortZoneId; + + /** 需要转换的时区 (默认转换为 PST美国洛杉矶时区)*/ + @Value("${shortZoneId.convert:PST}") + private String convertShortZoneId; + + /** + * 配置对http request,response的converter + * 1. 使用fastjson来序列化/反序列化json string + * 2. 全局配置fastjson对LocalDateTime, LocalDate的序列化/反序列化 + */ + @Override + public void configureMessageConverters(List> converters) { + FastJsonConfig fastJsonConfig = new FastJsonConfig(); + fastJsonConfig.setSerializerFeatures(FastjsonUtils.SERIALIZER_FEATURES); + + // 支持Actuator. Actuator返回的数据content-type是application/vnd.spring-boot.actuator.v2+json + MediaType application = new MediaType("application", "*+json"); + + FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); + fastJsonHttpMessageConverter.setSupportedMediaTypes(Lists.newArrayList(MediaType.APPLICATION_JSON_UTF8, application)); + fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig); + converters.add(0, fastJsonHttpMessageConverter); + + // 适用于直接原文返回数据的response @RequestMapping(value = "/xxx", produces = "text/plain;charset=utf-8") + StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); + stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN)); + converters.add(1, stringHttpMessageConverter); + + // support yml + YamlHttpMessageConverter yamlHttpMessageConverter = new YamlHttpMessageConverter(); + yamlHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(YamlHttpMessageConverter.MEDIA_TYPE_YAML, YamlHttpMessageConverter.MEDIA_TYPE_YML)); + converters.add(2, yamlHttpMessageConverter); + + initFastJsonGlobalConfig(); + } + + private void initFastJsonGlobalConfig() { + // 全局配置, 为LocalDateTime添加专用的serializer + SerializeConfig.getGlobalInstance().put(LocalDateTime.class, (serializer, object, fieldName, fieldType, features) -> { + if (object == null) { + serializer.out.writeNull(); + return; + } + long value = ((LocalDateTime) object).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + serializer.out.writeLong(value); + }); + + // 全局配置, 为LocalDate添加专用的serializer + SerializeConfig.getGlobalInstance().put(LocalDate.class, (serializer, object, fieldName, fieldType, features) -> { + if (object == null) { + serializer.out.writeNull(); + return; + } + long value = ((LocalDate) object).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(); + serializer.out.writeLong(value); + }); + + // 初始状态的ParserConfig.deserializers中没有LocalDateTime.class对应的deserializer + // 会造成直接从timestamp反序列化到LocalDateTime失败;需要预设成默认的Jdk8DateCodec + ParserConfig.getGlobalInstance().putDeserializer(LocalDateTime.class, Jdk8DateCodec.instance); + } + + /** + * 配置formatter,将API入参中的timestamp转化为LocalDateTime或者LocalDate + * + * @param registry + */ + @Override + public void addFormatters(FormatterRegistry registry) { + super.addFormatters(registry); + //XXX: 不能使用lambda + registry.addConverter(new Converter() { + @Override + public LocalDateTime convert(String source) { + if (Strings.isNullOrEmpty(source)) { + return null; + } + return LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(source)), ZoneId.systemDefault()); + } + }); + + registry.addConverter(new Converter() { + @Override + public LocalDate convert(String source) { + if (Strings.isNullOrEmpty(source)) { + return null; + } + return Instant.ofEpochMilli(Long.parseLong(source)).atZone(ZoneId.systemDefault()).toLocalDate(); + } + }); + } + + /** + * 添加拦截器 + * + * @param registry + */ + @Override + protected void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new TraceInterceptor(appRuntime)); + // 跨域的拦截器, 并设置最高优先级. 避免抛出异常时跨域未生效 + // 不建议使用DelegatingWebMvcConfiguration.addCorsMappings. 使用该方法配置之后再使用自定义拦截器时跨域相关配置就会失效 + registry.addInterceptor(new HandlerInterceptor() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + //优先使用请求传入的ORIGIN, 参考org.springframework.web.cors.DefaultCorsProcessor#handleInternal + String origin = StringUtils.firstNonBlank(request.getHeader(HttpHeaders.ORIGIN), CorsConfiguration.ALL); + if (response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN).isEmpty()) { + response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); + } + // 前端手动设置了 headers: {content-type: 'application/json'},在 OPTIONS 预请求的时候需要指定允许 + // 反向代理后,需要设置auth-tk + if (response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS).isEmpty()) { + response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "content-type,auth-tk,*"); + } + response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, CorsConfiguration.ALL); + if (response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS).isEmpty()) { + response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE.toString()); + } + response.addHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_MAX_AGE); + return true; + } + }); + + super.addInterceptors(registry); + + if (null != handlerInterceptors) { + Arrays.stream(handlerInterceptors).forEach(e -> { + registry.addInterceptor(e); + }); + } + registry.addInterceptor(new RequestContextInterceptor()); + } + + /** + * 添加ArgumentResolver,以便在Controller中直接获取argument + * + * @param argumentResolvers + */ + @Override + protected void addArgumentResolvers(List argumentResolvers) { + super.addArgumentResolvers(argumentResolvers); + if (null != handlerMethodArgumentResolvers) { + Arrays.stream(handlerMethodArgumentResolvers).forEach(e -> { + argumentResolvers.add(e); + }); + } + //XXX 用于controller方法参数列表直接获取AuthCaller + argumentResolvers.add(new AuthCallerResolver()); + argumentResolvers.add(new OriginalCallerResolver()); + argumentResolvers.add(new SessionResolver()); + } + + /** + * 显示指定spring mvc使用的validator, 避免在方法签名中使用@Validated不生效 + * + * @return + */ + @Override + protected Validator getValidator() { + return Optional.ofNullable(validator).orElse(super.getValidator()); + } + + @Override + public void afterPropertiesSet() throws Exception { + //设置找不到mapping时抛出异常, 避免直接返回错误页面 + servlet.setThrowExceptionIfNoHandlerFound(true); + + //初始化数据到时间转换工具类中 + DateConvertUtils.systemShortZoneId = systemShortZoneId; + DateConvertUtils.convertShortZoneId = convertShortZoneId; + } + + /** + * 全局的异常处理,当API调用抛出异常后,由该GlobalExceptionHandler处理 + */ + @ControllerAdvice + static class GlobalExceptionHandler extends AbstractExceptionHandler { + public GlobalExceptionHandler(ExceptionHandlerHook hook) { + super(hook); + } + } + + /** + * RequestContext保存当前Http Request + * + * @return RequestContext + */ + @Bean + public HttpRequestContext requestContext() { + return new HttpRequestContext(); + } + + /** + * 获取当前服务AppId的Hook + * + * @param appRuntime + * @return + */ + @Bean + @ConditionalOnMissingBean + AbstractExceptionHandler.ExceptionHandlerHook exceptionHandlerHook(AppRuntime appRuntime) { + return () -> appRuntime.getAppId(); + } + + /** + * copy from https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/yaml-msg-converter.html + * @param + */ + private static class YamlHttpMessageConverter extends AbstractHttpMessageConverter { + static final MediaType MEDIA_TYPE_YAML = MediaType.valueOf("text/yaml"); + static final MediaType MEDIA_TYPE_YML = MediaType.valueOf("text/yml"); + + @Override + protected boolean supports (Class clazz) { + return true; + } + + @Override + protected T readInternal (Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + Yaml yaml = new Yaml(); + Iterable itr = yaml.loadAll(inputMessage.getBody()); + + Object itrNext = itr.iterator().next(); + if (itrNext instanceof Collection) { + return (T) new JSONArray().fluentAddAll((Collection) itrNext).toJavaList(JSONObject.class); + } + + // find the root which is a map + return new JSONObject((Map)itrNext).toJavaObject(clazz); + } + + @Override + protected void writeInternal (T t, HttpOutputMessage outputMessage) + throws HttpMessageNotWritableException { + throw new UnsupportedOperationException("not convert object to yml"); + } + } + + /** + * 发现如果继承了WebMvcConfigurationSupport,则在yml中配置的相关内容会失效。 需要重新指定静态资源 + * + * @param registry + */ + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // swagger 资源 + registry.addResourceHandler("/**").addResourceLocations( + "classpath:/static/"); + registry.addResourceHandler("swagger-ui.html").addResourceLocations( + "classpath:/META-INF/resources/"); + registry.addResourceHandler("/webjars/**").addResourceLocations( + "classpath:/META-INF/resources/webjars/"); + // 有什么需要自测的网页,可以放在这个目录下 + registry.addResourceHandler("/**").addResourceLocations( + "classpath:/templates/"); + super.addResourceHandlers(registry); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/context/HttpRequestContext.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/context/HttpRequestContext.java new file mode 100644 index 0000000..bd7e389 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/context/HttpRequestContext.java @@ -0,0 +1,26 @@ +package com.sonic.common.context; + +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; + +/** + * http请求的context对象. 方便在服务层访问到http request信息. + * @author code + */ +public class HttpRequestContext { + + private static ThreadLocal threadLocal = new InheritableThreadLocal(); + + public static void set(HttpServletRequest request) { + threadLocal.set(request); + } + + public static Optional get() { + return Optional.ofNullable(threadLocal.get()); + } + + public static void remove() { + threadLocal.remove(); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/context/RequestContextInterceptor.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/context/RequestContextInterceptor.java new file mode 100644 index 0000000..b780263 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/context/RequestContextInterceptor.java @@ -0,0 +1,23 @@ +package com.sonic.common.context; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * @author code + */ +public class RequestContextInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + HttpRequestContext.remove(); + HttpRequestContext.set(request); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + HttpRequestContext.remove(); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/enums/AppEnv.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/enums/AppEnv.java new file mode 100644 index 0000000..e16963a --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/enums/AppEnv.java @@ -0,0 +1,16 @@ +package com.sonic.common.enums; + +/** + * 应用当前的环境 + * @author code + */ +public enum AppEnv { + /** 使用小写保持和 profile 定义一致 */ + product, + test, + dev, + local, + unittest, + staging, + ; +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/AbstractEventProducer.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/AbstractEventProducer.java new file mode 100644 index 0000000..5915970 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/AbstractEventProducer.java @@ -0,0 +1,113 @@ +package com.sonic.common.event; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +import org.springframework.beans.BeanUtils; +import org.springframework.core.task.TaskExecutor; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import lombok.extern.slf4j.Slf4j; + +/** + * 默认EventProducer,抽象里事件的组装和事务后发送的逻辑,提供一个Sender,具体服务具体提供 + */ +@Slf4j +public abstract class AbstractEventProducer implements EventProducer { + + private final AfterCommitExecutorImpl afterCommitExecutor; + + private final TaskExecutor taskExecutor; + + /** + * @return 返回发送消息的发送器 + */ + public abstract BiConsumer getSender(); + + private M defaultMeta; + + public AbstractEventProducer(M defaultMeta, TaskExecutor taskExecutor) { + this.afterCommitExecutor = new AfterCommitExecutorImpl(); + this.taskExecutor = taskExecutor; + this.defaultMeta = defaultMeta; + } + + @Override + public void send(Event event, M m) { + // XXX:不要在send的时候修改event的值,有副作用。 + // 例如:当将同一个event发送到不同的topic的时候,buildSchemaHash会用不同的topic赋值两次,导致一些异常case + Event copiedEvent = Event.builder().build(); + BeanUtils.copyProperties(event, copiedEvent); + afterCommitExecutor.execute(() -> taskExecutor.execute(() -> { + try { + log.info("====MQ PRODUCER ====, meta={}, message = {}", m, copiedEvent.toJsonString()); + getSender().accept(copiedEvent, m); + } catch (Exception e) { + log.error("====MQ PRODUCER ====, meta={}, message = {}", m, copiedEvent.toJsonString(), e); + } + })); + } + + @Override + public void send(Event event) { + send(event, defaultMeta); + } + + /** + * stolen from http://azagorneanu.blogspot.jp/2013/06/transaction-synchronization-callbacks.html + * 保证在交易结束后被调用. + */ + private static class AfterCommitExecutorImpl extends TransactionSynchronizationAdapter { + private static final ThreadLocal> RUNNABLES = new ThreadLocal>(); + + public void execute(Runnable runnable) { + if (log.isInfoEnabled()) { + log.info("Submitting new runnable {} to run after commit", runnable); + } + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + if (log.isInfoEnabled()) { + log.info("Transaction synchronization is NOT ACTIVE. Executing right now runnable {}", runnable); + } + runnable.run(); + return; + } + List threadRunnables = RUNNABLES.get(); + if (threadRunnables == null) { + threadRunnables = new ArrayList<>(); + RUNNABLES.set(threadRunnables); + TransactionSynchronizationManager.registerSynchronization(this); + } + threadRunnables.add(runnable); + } + + @Override + public void afterCommit() { + List threadRunnables = RUNNABLES.get(); + if (log.isInfoEnabled()) { + log.info("Transaction successfully committed, executing {} runnables", threadRunnables.size()); + } + for (int i = 0; i < threadRunnables.size(); i++) { + Runnable runnable = threadRunnables.get(i); + if (log.isInfoEnabled()) { + log.info("Executing runnable {}", runnable); + } + try { + runnable.run(); + } catch (RuntimeException e) { + log.error("Failed to execute runnable " + runnable, e); + } + } + } + + @Override + public void afterCompletion(int status) { + if (log.isInfoEnabled()) { + log.info("Transaction completed with status {}", status == TransactionSynchronization.STATUS_COMMITTED ? "COMMITTED" : "ROLLED_BACK"); + } + RUNNABLES.remove(); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/DefaultEventConsumer.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/DefaultEventConsumer.java new file mode 100644 index 0000000..c142d68 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/DefaultEventConsumer.java @@ -0,0 +1,88 @@ +package com.sonic.common.event; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import com.alibaba.fastjson.JSONObject; + +import lombok.extern.slf4j.Slf4j; + +/** + * 消费消息,用法如下: + *
+ * // EVENT_TOPIC_KEY = "topic"
+ * @Bean
+ *     EventConsumer eventConsumer(AppRuntime appRuntime, EventHandlerRepository eventHandlerRepository, ApiStatsClient apiStatsClient) {
+ *         Consumer callback = (eventWrapper) -> {
+ *             if (eventWrapper.isHandled()) {
+ *                 // 只收集被App真正消费的消息.
+ *                 String topic = (String)eventWrapper.getExt().get(EVENT_TOPIC_KEY);
+ *                 apiStatsClient.reportConsumedEvent(eventWrapper.getEvent(), topic);
+ *             }
+ *         };
+ *         return new DefaultEventConsumer(appRuntime.getAppName(), eventHandlerRepository, callback);
+ *     }
+ * 
+ */ +@Slf4j +public class DefaultEventConsumer implements EventConsumer { + + private final EventHandlerRepository handlerRepository; + + private final String appName; + private final Consumer consumeCallback; + + /** + * 推荐使用 带callback的构造方法 可以通过callback集成apiStatsClient对消息进行治理
+ * 见:{@link DefaultEventConsumer#DefaultEventConsumer(java.lang.String, EventHandlerRepository, java.util.function.Consumer)} + */ + @Deprecated + public DefaultEventConsumer(String appName, EventHandlerRepository handlerRepository) { + this(appName, handlerRepository, null); + } + + public DefaultEventConsumer(String appName, EventHandlerRepository handlerRepository, Consumer consumeCallback) { + this.handlerRepository = handlerRepository; + this.appName = appName; + this.consumeCallback = consumeCallback; + } + + @Override + public void onEvent(String message) { + onEvent(message, Collections.emptyMap()); + + } + + @Override + public boolean onEvent(String message, Map ext) { + Event event; + // 默认开启日志,如需关闭日志,在调用onEvent的时候,设置logEnable为false + boolean logEnable = ext == null || Boolean.TRUE.equals(ext.getOrDefault("logEnabled", true)); + if (logEnable) { + log.info("====MQ CONSUMER===={} , message = {}", appName, message); + } + try { + event = JSONObject.parseObject(message, Event.class); + } catch (Exception e) { + log.error("====MQ CONSUMER===={}, parse event error, event = {}", appName, message, e); + return false; + } + boolean handled = handlerRepository.process(event, ext); + if (consumeCallback != null) { + consumeCallback.accept(EventWrapper.builder().event(event).consumer(this).isHandled(handled).ext(ext).build()); + } + return handled; + } + + @Override + public EventHandlerRepository registerHandler(Event.EventCode eventCode, EventHandler eventHandler) { + return handlerRepository.registerHandler(eventCode, eventHandler); + } + + @Override + public EventHandlerRepository registerHandlers(List eventCodes, EventHandler eventHandler) { + return handlerRepository.registerHandlers(eventCodes, eventHandler); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/Event.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/Event.java new file mode 100644 index 0000000..6f32024 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/Event.java @@ -0,0 +1,193 @@ +package com.sonic.common.event; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.annotation.JSONField; +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.hash.Hashing; +import com.sonic.common.utils.FastjsonUtils; +import lombok.*; +import org.apache.commons.lang3.StringUtils; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.cglib.beans.BeanMap; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 统一系统事件对象. + * 消息格式如下 + * { + * "eventId": "0101_xxx" //消息id + * "eventScene":"teaching", + * "eventModule":"im", + * "eventName":"message_sent", + * "eventTime":"2019-07-22 16:10:10.666", + * "data":{"message_id":"xxx","tags":"xxx"} //消息ID,消息标签 + * "schemaHash": "xxxx" //数据的schema hash + * } + * @author code + */ +@Data +@AllArgsConstructor +public class Event { + + /** + * 唯一标识.唯一区分一条事件.可以做幂等处理. 默认是app_id + uuid + */ + private String eventId; + + /** + * 使用字符串,方便扩展。
+ * 建议优先参考{@link BuildInScene}这个枚举值中是否有合适的code + */ + private String eventScene; + + private String eventModule; + + private String eventName; + + @JSONField(format = "yyyy-MM-dd'T'HH:mm:ss.SSS") + private LocalDateTime eventTime; + + private MessagePostProcessor messagePostProcessor; + + private Object data; + + /** + * event的schema hash. 在发送的时候填充. + * 接受方可以通过简单的检查这个标示来判断event的scheme是否发生变化. + */ + @Setter(AccessLevel.PACKAGE) + private String schemaHash; + + /** + * 提供默认构造函数,仅用于json反序列化时使用
+ * 不建议直接使用默认构造函数进行event的构建。构建event,请使用Event.builder().xx(xx).build(); + * XXX: 此处不使用@NoArgsConstrutor是因为其于@Builder.Default会产生冲突。; + * @Deprecated + */ + public Event(){ + } + + @Builder + public Event(String eventScene, String eventModule, String eventName, Object data) { + this.eventScene = eventScene; + this.eventModule = eventModule; + this.eventName = eventName; + this.data = data; + + this.eventTime = LocalDateTime.now(); + } + + public Event(String eventScene, String eventModule, String eventName, MessagePostProcessor messagePostProcessor, Object data) { + this.eventScene = eventScene; + this.eventModule = eventModule; + this.eventName = eventName; + this.messagePostProcessor = messagePostProcessor; + this.data = data; + + this.eventTime = LocalDateTime.now(); + } + + public String toJsonString() { + return JSONObject.toJSONString(this, FastjsonUtils.SERIALIZER_FEATURES); + } + + @Override + public String toString() { + return toJsonString(); + } + + /** + * 该方法返回一个字符串,该字符串可用于唯一标注一种类型的消息。
+ * 对于kafka,可以将该值用于消息的key,
+ * 对于rabbit,可以将该值用于消息的routingKey。
+ * 以方便消费方快速获取消息类型 + * + * @return + */ + @JSONField(serialize = false) + public EventCode getEventCode() { + return new EventCode(eventScene, eventModule, eventName); + } + + @JSONField(serialize = false) + public String getAppId() { + return StringUtils.left(eventId, 4); + } + + /** + * 发送前,对必填参数做校验 + */ + public void check() { + Preconditions.checkArgument(!Strings.isNullOrEmpty(eventScene), "发送消息 -> 事件场景不能为空"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(eventModule), "发送消息 -> 事件模块不能为空"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(eventName), "发送消息 -> 事件名字不能为空"); + } + + public T normalizedData(Class tClass) { + return JSON.parseObject(JSON.toJSONString(data), tClass); + } + + /** + * 构建消息的schema hash + * @param routingKey rabbitmq 的 routingKey + * @return schema hash + */ + public String buildSchemaHash(String routingKey) { + String schema; + // FIXME: beanmap不能处理data是集合类型对象. 这些需要特殊处理 + // 建议data总是一个bean对象. + Object data = getData(); + if (data == null) { + schema = "null"; + } else if (data instanceof Collection + || data instanceof Map + || data instanceof Number + || data.getClass().isArray() + || data.getClass().isPrimitive() + || data instanceof String) { + schema = data.getClass().getCanonicalName(); + } else { + try { + BeanMap beanMap = BeanMap.create(data); + // 取payload中每个key作为schema + schema = beanMap.keySet().stream().map(e -> e + "=" + beanMap.getPropertyType((String) e).getSimpleName()) + .sorted().collect(Collectors.joining(",")).toString(); + } catch (Exception e) { + // ignore + schema = "error"; + } + } + return Hashing.murmur3_128().hashString(routingKey + getEventScene() + getEventModule() + + getEventName() + schema, Charsets.UTF_8).toString(); + } + + /** + * 消息发生场景 + */ + @AllArgsConstructor + @Getter + public enum BuildInScene { + /** basic services 基础服务 */ + BS("bs"), + SONIC("sonic"), + ; + private final String code; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class EventCode { + private String scene; + private String module; + private String name; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventConsumer.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventConsumer.java new file mode 100644 index 0000000..f094aed --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventConsumer.java @@ -0,0 +1,56 @@ +package com.sonic.common.event; + +import java.util.List; +import java.util.Map; + +import lombok.Builder; +import lombok.Getter; + +public interface EventConsumer { + + /** + * 注册EventHandler + * + * @param eventCode event类型的唯一标示 + * @param eventHandler eventHandler + * @return EventHandlerRepository + */ + EventHandlerRepository registerHandler(Event.EventCode eventCode, EventHandler eventHandler); + + /** + * 为多个eventCodes注册一个eventHandler + * + * @param eventCodes list of eventCode + * @param eventHandler eventHandler + * @return EventHandlerRepository + */ + EventHandlerRepository registerHandlers(List eventCodes, EventHandler eventHandler); + + /** + * message的处理方法 + * @param message message + * @see EventConsumer#onEvent(String, Map) + * @deprecated 不推荐使用. {@link EventConsumer#onEvent(String, Map)} + */ + @Deprecated + void onEvent(String message); + + /** + * message的处理方法 + * + * @param message message + * @Param ext 扩展信息. + * @return 如果事件被handler处理返回true, 否则返回false + */ + boolean onEvent(String message, Map ext); + + @Builder + @Getter + class EventWrapper { + Event event; + EventConsumer consumer; + boolean isHandled; + Map ext; + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventHandler.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventHandler.java new file mode 100644 index 0000000..0d8b9cf --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventHandler.java @@ -0,0 +1,14 @@ +package com.sonic.common.event; + +/** + * @author code + */ +public interface EventHandler { + + /** + * 事件处理 + * @param event 事件内容 + */ + void onEvent(Event event); + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventHandlerRepository.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventHandlerRepository.java new file mode 100644 index 0000000..bfa6a81 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventHandlerRepository.java @@ -0,0 +1,96 @@ +package com.sonic.common.event; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.GlobalResultCode; +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; + +import lombok.extern.slf4j.Slf4j; + +/** + * 事件处理器仓库 + * 可以在多个地方使用. + */ +@Slf4j +public class EventHandlerRepository { + final protected ListMultimap handlers = ArrayListMultimap.create(); + private final BiConsumer exceptionHandler; + + public EventHandlerRepository(BiConsumer exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + public EventHandlerRepository() { + this(null); + } + + public EventHandlerRepository registerHandler(Event.EventCode eventCode, EventHandler eventHandler) { + Objects.requireNonNull(eventCode); + Objects.requireNonNull(eventHandler); + + handlers.put(eventCode, eventHandler); + return this; + } + + public EventHandlerRepository registerHandlers(List eventCodes, EventHandler eventHandler) { + Objects.requireNonNull(eventHandler); + eventCodes.forEach(e -> handlers.put(e, eventHandler)); + return this; + } + + /** + * 处理事件 + * @param event 事件 + * @return 如果有注册的handler处理事件返回true, 否则false + */ + public boolean process(Event event, Map ext) { + Stopwatch stopwatch = Stopwatch.createUnstarted(); + List eventHandlers = handlers.get(event.getEventCode()); + eventHandlers.stream().forEach(handler -> { + try { + stopwatch.start(); + String clazzName = handler.getClass().getCanonicalName(); + log.info("====MQ CONSUMER====, start handling by {}", clazzName); + handler.onEvent(event); + + long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); + log.info("====MQ CONSUMER====, handled by {}, cost {} millis", clazzName, elapsed); + + Number maxElapsed = (Number) ext.getOrDefault("maxElapsedMillis", 10_000); + if (elapsed > maxElapsed.longValue()) { + String msg = String.format("take too long %d millis for %s to handle %s", + elapsed, clazzName, event.toJsonString()); + handleException(new BizException(GlobalResultCode.EVENT_HANDLED_TIMEOUT), msg); + } + } catch (Exception ex) { + log.error("====MQ CONSUMER====, handle event error, event = {}", event.toJsonString(), ex); + handleException(ex, event.toJsonString()); + } finally { + // stopwatch必须reset(),否则下一次stopwatch.start()会报错 + stopwatch.reset(); + } + }); + return !eventHandlers.isEmpty(); + } + + private void handleException(Exception ex, String msg) { + if (exceptionHandler != null) { + exceptionHandler.accept(ex, msg); + } + } + + public boolean isHandled(Event event) { + Preconditions.checkArgument(event != null); + Preconditions.checkArgument(event.getEventCode() != null); + + return handlers.containsKey(event.getEventCode()); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventProducer.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventProducer.java new file mode 100644 index 0000000..989a047 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/EventProducer.java @@ -0,0 +1,18 @@ +package com.sonic.common.event; + +public interface EventProducer { + + /** + * 发送event + * + * @param event 事件主体 + */ + void send(Event event, Meta meta); + + /** + * 发送event,meta信息使用默认的meta + * + * @param event + */ + void send(Event event); +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/RabbitmqEventProducer.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/RabbitmqEventProducer.java new file mode 100644 index 0000000..0277cb2 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/event/RabbitmqEventProducer.java @@ -0,0 +1,126 @@ +package com.sonic.common.event; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.core.task.TaskExecutor; +import org.springframework.util.CollectionUtils; + +import com.google.common.base.Strings; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 发送消息 + */ +public class RabbitmqEventProducer extends AbstractEventProducer { + + private RabbitTemplate rabbitTemplate; + + private String defaultModule; + private String defaultScene; + private String appId; + /** + * 一个扩展点.让外部可以在发送消息后做特殊逻辑处理. 比如发送消息统计, 消息元数据监控 + */ + private BiConsumer sendCallback; + + /** + * 推荐使用 带callback的构造方法 可以通过callback集成apiStatsClient对消息进行治理
+ * 见:{@link RabbitmqEventProducer#RabbitmqEventProducer(org.springframework.amqp.rabbit.core.RabbitTemplate, String, String, String, RabbitmqMessageMeta, TaskExecutor, BiConsumer)} + */ + @Deprecated + public RabbitmqEventProducer(RabbitTemplate rabbitTemplate, String defaultModule, String defaultScene, + String appId, RabbitmqMessageMeta defaultMeta, TaskExecutor taskExecutor) { + this(rabbitTemplate, defaultModule, defaultScene, appId, defaultMeta, taskExecutor, null); + } + + public RabbitmqEventProducer(RabbitTemplate rabbitTemplate, String defaultModule, String defaultScene, + String appId, RabbitmqMessageMeta defaultMeta, TaskExecutor taskExecutor, + BiConsumer sendCallback) { + super(defaultMeta, taskExecutor); + this.rabbitTemplate = rabbitTemplate; + this.defaultModule = defaultModule; + this.defaultScene = defaultScene; + this.appId = appId; + this.sendCallback = sendCallback; + } + + @Override + public BiConsumer getSender() { + return (event, meta) -> { + // correct scene and module if it's empty + if (Strings.isNullOrEmpty(event.getEventScene())) { + event.setEventScene(defaultScene); + } + if (Strings.isNullOrEmpty(event.getEventModule())) { + event.setEventModule(defaultModule); + } + if (Strings.isNullOrEmpty(event.getEventId())) { + // init eventId -> appId_uuid + event.setEventId(String.format("%s_%s", appId, UUID.randomUUID().toString().replace("-", ""))); + } + + event.setSchemaHash(event.buildSchemaHash(meta.getRoutingKey())); + event.check(); + + final MessageProperties properties = new MessageProperties(); + properties.setContentType(MessageProperties.CONTENT_TYPE_JSON); + // 设置5天有效期.避免废弃的队列一直接受数据. + properties.setExpiration("" + TimeUnit.DAYS.toMicros(5)); + if (!CollectionUtils.isEmpty(meta.getHeaders())) { + meta.getHeaders().entrySet() + .forEach(entry -> properties.getHeaders().put(entry.getKey(), entry.getValue())); + } + // XXX: must make sure the transaction is commit before send user message!!! + + if(event.getMessagePostProcessor() != null) { + rabbitTemplate.convertAndSend(meta.getExchange(), meta.getRoutingKey(), event.toJsonString(), event.getMessagePostProcessor()); + } else { + rabbitTemplate.convertAndSend(meta.getExchange(), meta.getRoutingKey(), event.toJsonString()); + } + if (sendCallback != null) { + sendCallback.accept(event, meta); + } + }; + } + + @NoArgsConstructor + @Getter + @ToString + public static class RabbitmqMessageMeta { + private String exchange; + private String routingKey; + private Map headers; + + public static RabbitmqMessageMeta build(String exchange, String routingKey, Map headers) { + RabbitmqMessageMeta meta = new RabbitmqMessageMeta(); + meta.exchange = exchange; + meta.routingKey = routingKey; + meta.headers = Optional.ofNullable(headers).orElseGet(HashMap::new); + return meta; + } + + public static RabbitmqMessageMeta build(String exchange, String routingKey) { + return build(exchange, routingKey, null); + } + + public static RabbitmqMessageMeta build(String topic) { + return build(topic, null, null); + } + + public RabbitmqMessageMeta header(String key, byte[] value) { + headers.put(key, value); + return this; + } + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/AbstractExceptionHandler.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/AbstractExceptionHandler.java new file mode 100644 index 0000000..3a36e53 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/AbstractExceptionHandler.java @@ -0,0 +1,259 @@ +package com.sonic.common.exception; + +import java.util.Map; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.ConstraintViolationException; +import javax.validation.Path; + +import com.sonic.common.auth.AuthInterceptor; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.rpc.GlobalResultCode; +import com.sonic.common.rpc.Result; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.TypeMismatchException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import com.alibaba.fastjson.JSONObject; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +/** + * @author code + */ +@Slf4j +public abstract class AbstractExceptionHandler { + private static final Pattern SYSTEM_PATTERN = Pattern.compile("^.*\\(\\d+\\)$"); + + private String appId; + + private ExceptionHandlerHook exceptionHook; + + public AbstractExceptionHandler(ExceptionHandlerHook hook) { + Preconditions.checkArgument(hook != null); + Preconditions.checkArgument(!Strings.isNullOrEmpty(hook.getAppId())); + + this.appId = hook.getAppId(); + this.exceptionHook = hook; + } + + @ExceptionHandler(SysException.class) + @ResponseBody + public Result sysExceptionHandler(HttpServletRequest request, HttpServletResponse response, SysException e) { + return Result.error(e.getErrorCode(), buildErrorMessage(e.getErrorMsg())); + } + + @ExceptionHandler(BizException.class) + @ResponseBody + public Result businessExceptionHandler(HttpServletRequest request, HttpServletResponse response, BizException e) { + return Result.error(e.getErrorCode(), buildErrorMessage(e.getErrorMsg())); + } + + @ExceptionHandler(Exception.class) + @ResponseBody + public Result defaultExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception e) { + // 未捕获异常不需要在该处打印日志, 由 RequestLogAspect 处理 + ExceptionContext exceptionContext = new ExceptionContext(request, response, e, GlobalResultCode.SYSTEM_EXCEPTION); + exceptionHook.getExceptionConsumer().accept(exceptionContext); + + return Result.error(exceptionContext.getResultCode().getErrorCode(), + buildErrorMessage(exceptionContext.getResultCode().getErrorMsg())); + } + + /** + * 支持内部抛出的 IllegalArgumentException, 如 com.google.common.base.Preconditions.checkArgument() 抛出的异常 + * @param response + * @param e + * @return + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseBody + public Result illegalArgumentExceptionHandler(HttpServletResponse response, IllegalArgumentException e) { + return Result.error(GlobalResultCode.SYSTEM_EXCEPTION, buildErrorMessage(Strings.isNullOrEmpty(e.getMessage()) ? "parameter error" : e.getMessage())); + } + + /** + * 404 请求path无效 + * @param response + * @return + */ + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseBody + public Result noHandlerExceptionHandler(HttpServletResponse response) { + return Result.error(GlobalResultCode.SYSTEM_EXCEPTION, buildErrorMessage("request path error")); + } + + /** + * 请求 body 未设置或者 JSONObject.parse 参数类型失败 + * @param response + * @param e + * @return + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseBody + public Result httpMessageNotReadableExceptionHandler(HttpServletResponse response, HttpMessageNotReadableException e) { + log.error("Invalid request, parameter error",e); + return Result.error(GlobalResultCode.INVALID_PARAMS, buildErrorMessage("Invalid request, parameter error")); + } + + + + /** + * LocalDateTime 等类型错误 + * @param response + * @return + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseBody + public Result typeMismatchExceptionHandler(HttpServletResponse response) { + return Result.error(GlobalResultCode.INVALID_PARAMS, buildErrorMessage("Invalid request, parameter type error")); + } + + /** + * 包装类型参数缺失 + * @param response + * @param e + * @return + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseBody + public Result constraintViolationExceptionHandler(HttpServletResponse response, ConstraintViolationException e) { + return Result.error(GlobalResultCode.INVALID_PARAMS, + buildErrorMessage(e.getConstraintViolations().stream() + .findFirst() + .map(ex -> buildPrettyMessage(shortPropertyPath(ex.getPropertyPath()) + " " + ex.getMessage())) + .orElse(e.getMessage()))); + } + + /** + * 对象类型参数错误 + * @param response + * @param e + * @return + */ + @ExceptionHandler(BindException.class) + @ResponseBody + public Result bindExceptionExceptionHandler(HttpServletResponse response, BindException e) { + FieldError fieldError = e.getBindingResult().getFieldError(); + if (fieldError.contains(TypeMismatchException.class)) { + return Result.error(GlobalResultCode.INVALID_PARAMS.getErrorCode(), + buildErrorMessage(String.format("parameter type error, parameter name: %s", fieldError.getField()))); + } + return Result.error(GlobalResultCode.INVALID_PARAMS.getErrorCode(), + buildErrorMessage(fieldError.getField() + " " + fieldError.getDefaultMessage())); + } + + /** + * 包装类型参数绑定失败 + * @param response + * @param e + * @return + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseBody + public Result methodArgumentTypeMismatchExceptionNotReadable(HttpServletResponse response, MethodArgumentTypeMismatchException e) { + return Result.error(GlobalResultCode.INVALID_PARAMS, + buildErrorMessage(String.format("parameter type error, parameter name: %s", e.getName()))); + } + + /** + * 包装类型参数校验失败 + * @param response + * @param e + * @return + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseBody + public Result methodArgumentNotValidExceptionExceptionNotReadable(HttpServletResponse response, MethodArgumentNotValidException e) { + String errorMsg = e.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .orElse("parameter verification failed"); + return Result.error(GlobalResultCode.INVALID_PARAMS, buildErrorMessage(errorMsg)); + } + + /** + * MissingServletRequestParameterException 处理 @ RequestParam注解参数缺失的异常 + * @param response + * @param e + * @return Result + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseBody + public Result missingServletRequestParameterException(HttpServletResponse response, MissingServletRequestParameterException e) { + return Result.error(GlobalResultCode.INVALID_PARAMS, + buildErrorMessage(String.format("parameter lost[%s - %s]", e.getParameterName(), e.getParameterType()))); + } + + private String shortPropertyPath(Path path) { + String s = StringUtils.substringAfterLast(path.toString(), "."); + if (StringUtils.isEmpty(s)) { + // 如果没有".", 返回原始路径 + return path.toString(); + } + return s; + } + + private String buildPrettyMessage(String message) { + return StringUtils.replace(message, "null", "空"); + } + + private String buildErrorMessage(String originMessage) { + // 原错误消息里已经有appId,那么不用再追加appId到后面 + boolean existAppId = SYSTEM_PATTERN.matcher(originMessage).find(); + + if (existAppId) { + return originMessage.substring(0, originMessage.length() - 6); + } + return originMessage; + } + + private static String dumpRequest(HttpServletRequest request) { + Map parameterMap = Maps.newHashMap(request.getParameterMap()); + return new JSONObject() + .fluentPut("path", request.getRequestURI()) + .fluentPut("caller", request.getAttribute(AuthInterceptor.CALLER_PAYLOAD_KEY)) + .fluentPut("body", request.getAttribute("params")) + .fluentPut("parameters", parameterMap) + .toJSONString(); + } + + public interface ExceptionHandlerHook { + String getAppId(); + + default Consumer getExceptionConsumer() { + return exceptionContext -> { + }; + } + } + + @Data + @AllArgsConstructor + public static class ExceptionContext { + HttpServletRequest request; + HttpServletResponse response; + Exception exception; + ApiResultCode resultCode; + + public String dumpRequest() { + return AbstractExceptionHandler.dumpRequest(request); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/BizException.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/BizException.java new file mode 100644 index 0000000..a2df4b5 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/BizException.java @@ -0,0 +1,44 @@ +package com.sonic.common.exception; + +import com.sonic.common.rpc.ApiResultCode; + +import lombok.Getter; + +/** + * 可预期的业务层面的反馈。 + * 与其说这个是异常,不如说是 message return。 + * 接口有两种返回方式,第一,业务实体,第二,exception message + * 当业务不满足条件,需要以 message 字符串的形式告诉调用方,这时候就可以通过 BizException 然后全局异常解析的方式来返回 + * @author code + */ +@Getter +public class BizException extends RuntimeException { + + private final String errorCode; + + private final String errorMsg; + + public BizException(ApiResultCode resultCode) { + super(String.format("BizException{errorCode:%s, errorMsg:%s}", resultCode.getErrorCode(), resultCode.getErrorMsg())); + this.errorCode = resultCode.getErrorCode(); + this.errorMsg = resultCode.getErrorMsg(); + } + + public BizException(ApiResultCode resultCode, Throwable cause) { + super(String.format("BizException{errorCode:%s, errorMsg:%s}", resultCode.getErrorCode(), resultCode.getErrorMsg()), cause); + this.errorCode = resultCode.getErrorCode(); + this.errorMsg = resultCode.getErrorMsg(); + } + + public BizException(String errorCode, String errorMsg) { + super(String.format("BizException{errorCode:%s, errorMsg:%s}", errorCode, errorMsg)); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + public BizException(String errorCode, String errorMsg, Throwable cause) { + super(String.format("BizException{errorCode:%s, errorMsg:%s}", errorCode, errorMsg), cause); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/BizExceptionUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/BizExceptionUtils.java new file mode 100644 index 0000000..7efeadf --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/BizExceptionUtils.java @@ -0,0 +1,61 @@ +package com.sonic.common.exception; + +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.rpc.GlobalResultCode; +import lombok.experimental.UtilityClass; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * @author: chen + * @date: 2020/05/14 + * @Description: + * @version: 1.0.0 + */ +@UtilityClass +public class BizExceptionUtils { + + public static void check(boolean expect, String msg) { + if (expect) { + throw new BizException(GlobalResultCode.INVALID_PARAMS.getErrorCode(), msg); + } + } + + public static void check(boolean expect, String code, String msg) { + if (expect) { + throw new BizException(code, msg); + } + } + + public static void check(boolean expect, ApiResultCode resultCode) { + if (expect) { + throw new BizException(resultCode); + } + } + + public static void check(boolean expect, Supplier supplier) throws T { + if (expect) { + throw supplier.get(); + } + } + + public static void checkEquals(Object source, Object target, String msg) { + if (!Objects.equals(source, target)) { + throw new BizException(GlobalResultCode.SYSTEM_EXCEPTION.getErrorCode(), msg); + } + } + + public static void checkNonNull(Object target, String msg) { + if (Objects.isNull(target)) { + throw new BizException(GlobalResultCode.SYSTEM_EXCEPTION.getErrorCode(), msg); + } + } + + public static void checkNotEmpty(Collection coll, String msg) { + if (coll == null || coll.isEmpty()) { + throw new BizException(GlobalResultCode.SYSTEM_EXCEPTION.getErrorCode(), msg); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/SysException.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/SysException.java new file mode 100644 index 0000000..c94393d --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/SysException.java @@ -0,0 +1,45 @@ +package com.sonic.common.exception; + +import com.sonic.common.rpc.ApiResultCode; + +import lombok.Getter; + +/** + * 可预期的业务层面的异常。 + * 例如,当第三方组件抛出非运行时异常时,可以转化为 SysException 抛出,由统一异常处理器处理 + * 与 {@link BizException} 的区别是, BizException 负责向调用端返回 message,而 SysException 核心是报告异常,然后向调用端返回 message + * 具体设计期望,BizException 在全局异常处理时,不会打印异常栈,不会发送邮件告警、短信告警等 + * 而 SysException 则会打印异常栈,且根据配置发出邮件告警或短信告警 + * @author code + */ +@Getter +public class SysException extends RuntimeException { + + private final String errorCode; + + private final String errorMsg; + + public SysException(ApiResultCode resultCode) { + super(String.format("BizException{errorCode:%s, errorMsg:%s}", resultCode.getErrorCode(), resultCode.getErrorMsg())); + this.errorCode = resultCode.getErrorCode(); + this.errorMsg = resultCode.getErrorMsg(); + } + + public SysException(ApiResultCode resultCode, Throwable cause) { + super(String.format("BizException{errorCode:%s, errorMsg:%s}", resultCode.getErrorCode(), resultCode.getErrorMsg()), cause); + this.errorCode = resultCode.getErrorCode(); + this.errorMsg = resultCode.getErrorMsg(); + } + + public SysException(String errorCode, String errorMsg) { + super(String.format("BizException{errorCode:%s, errorMsg:%s}", errorCode, errorMsg)); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + public SysException(String errorCode, String errorMsg, Throwable cause) { + super(String.format("BizException{errorCode:%s, errorMsg:%s}", errorCode, errorMsg), cause); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/SysExceptionUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/SysExceptionUtils.java new file mode 100644 index 0000000..7b3337b --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/exception/SysExceptionUtils.java @@ -0,0 +1,54 @@ +package com.sonic.common.exception; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Supplier; + +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.rpc.GlobalResultCode; + +import lombok.experimental.UtilityClass; + +/** + * @Description: + * @version: 1.0.0 + */ +@UtilityClass +public class SysExceptionUtils { + + public static void check(boolean expect, String code, String msg) { + if (expect) { + throw new SysException(code, msg); + } + } + + public static void check(boolean expect, ApiResultCode resultCode) { + if (expect) { + throw new SysException(resultCode); + } + } + + public static void check(boolean expect, Supplier supplier) throws T { + if (expect) { + throw supplier.get(); + } + } + + public static void checkEquals(Object source, Object target, String msg) { + if (!Objects.equals(source, target)) { + throw new SysException(GlobalResultCode.SYSTEM_EXCEPTION.getErrorCode(), msg); + } + } + + public static void checkNonNull(Object target, String msg) { + if (Objects.isNull(target)) { + throw new SysException(GlobalResultCode.SYSTEM_EXCEPTION.getErrorCode(), msg); + } + } + + public static void checkNotEmpty(Collection coll, String msg) { + if (coll == null || coll.isEmpty()) { + throw new BizException(GlobalResultCode.SYSTEM_EXCEPTION.getErrorCode(), msg); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/log/IgnoreRequestLog.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/log/IgnoreRequestLog.java new file mode 100644 index 0000000..e33fd42 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/log/IgnoreRequestLog.java @@ -0,0 +1,30 @@ +package com.sonic.common.log; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 忽略请求日志annotation. + * 默认所有的请求都会被通过拦截器拦截并打印日志信息.如果是敏感接口,或者文件流而不需要打印日志 + * 可以通过这个annotation来指定接口不需要打印. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface IgnoreRequestLog { + IgnoreType[] types() default {IgnoreType.REQUEST, IgnoreType.RESPONSE}; + + enum IgnoreType { + /** 忽略请求日志 */ + REQUEST, + /** 忽略响应日志 */ + RESPONSE, + /** SILENT -> 整个接口请求,不输出日志 */ + SILENT, + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/log/RequestLogAspect.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/log/RequestLogAspect.java new file mode 100644 index 0000000..922b182 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/log/RequestLogAspect.java @@ -0,0 +1,158 @@ +package com.sonic.common.log; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.auth.AuthCaller; +import com.sonic.common.auth.AuthInterceptor; +import com.sonic.common.auth.OriginalCaller; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.IpAddressUtils; +import com.sonic.common.utils.LogUtils; +import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableSet; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 统一打印日志(支持忽略此aop, 采用@IgnoreRequestLog注解) + * @author code + */ +@Aspect +@Slf4j +public class RequestLogAspect { + private final static Integer MAX_LOG_SIZE = 2048; + + public static final String REQUEST_ATTR_SESSION = "_session_"; + + + private static Set> EXCLUDE_CLASSES = ImmutableSet.of(ServletRequest.class, ServletResponse.class, + MultipartFile.class, AuthCaller.class, OriginalCaller.class); + + @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping) " + + "||@annotation(org.springframework.web.bind.annotation.PostMapping) " + + "||@annotation(org.springframework.web.bind.annotation.DeleteMapping) " + + "||@annotation(org.springframework.web.bind.annotation.PutMapping) " + + "||@annotation(org.springframework.web.bind.annotation.GetMapping) ") + public void pointCut() { + } + + + + @Around("pointCut()") + public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { + + + ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()); + if(servletRequestAttributes ==null){ + // 这里需要排除 fegin调用的情况 RequestContextHolder.getRequestAttributes() 不在当前线程的情况下 会造成空指针异常 ,例如 定时任务或者其他开的线程。 + return joinPoint.proceed(joinPoint.getArgs()); + } + + HttpServletRequest request = servletRequestAttributes.getRequest(); + String internalTraceId = request.getHeader("X-TRACE-ID"); + + if(StringUtils.isNotEmpty(internalTraceId)){ + LogUtils.setTraceId(internalTraceId); + }else{ + LogUtils.setTraceId(); + } + if (request != null) { + //用于在AbstractExceptionHandler中为dumpRequest, AbstractExceptionHandler无法直接获取body + request.setAttribute("params", this.getUserArgs(joinPoint.getArgs())); + } + + IgnoreRequestLog annotation = AnnotationUtils.findAnnotation(((MethodSignature) joinPoint.getSignature()).getMethod(), + IgnoreRequestLog.class); + + // 获取注解中的ignoreTypes + ImmutableSet ignoreTypes = Optional.ofNullable(annotation) + .map(a -> ImmutableSet.copyOf(a.types())).orElse(ImmutableSet.of()); + + Stopwatch stopwatch = Stopwatch.createStarted(); + Object caller = Optional.ofNullable(request.getAttribute(AuthInterceptor.CALLER_PAYLOAD_KEY)) + .map(JSON::toJSONString).orElse("UNKNOWN"); + Session session = (Session) request.getAttribute(REQUEST_ATTR_SESSION); + String userId = session == null || session.getUserId() == null ? "UNKNOWN" : session.getUserId().toString(); + String requestLog = buildRequestLog(joinPoint, ignoreTypes); + + Object proceed; + try { + proceed = joinPoint.proceed(joinPoint.getArgs()); + } catch (BizException e) { + //stein会收集error级别的异常并告警. 对BizException. 不期望收到告警邮件. 因此将BizException对应的日志级别调整为warn + log.warn("api log, process error, caught BizException, ip = {}, url = {}, caller = {}, userId = {}, params = {}, " + + "time cost = {} ms, BizException:", + IpAddressUtils.getIpAddress(request), request.getRequestURI(), caller, userId, requestLog, stopwatch.elapsed(TimeUnit.MILLISECONDS), e); + throw e; + } catch (Exception e) { + log.error("api log, process error, uncaught exception, ip = {}, url = {}, caller = {}, userId = {}, params = {}, " + + "time cost = {} ms, exception:", + IpAddressUtils.getIpAddress(request), request.getRequestURI(), caller, userId, requestLog, stopwatch.elapsed(TimeUnit.MILLISECONDS), e); + throw e; + } + + // 如果该接口定义的类型不是SILENT的, 则打印日志 + if (!ignoreTypes.contains(IgnoreRequestLog.IgnoreType.SILENT)) { + //响应时间 > 1s 时输出具体响应的秒数日志 1 - 9s 为等级1、>= 10s 为等级2 + long timeCost = stopwatch.elapsed(TimeUnit.MILLISECONDS); + log.info("api log, ip = {}, url = {}, caller = {}, params = {}, result = {}, time cost = {} ms{}", + IpAddressUtils.getIpAddress(request), + request.getRequestURI(), + caller, + requestLog, + buildResponseLog(proceed, ignoreTypes), + timeCost + ); + } + LogUtils.removeTraceId(); + return proceed; + } + + private String buildRequestLog(ProceedingJoinPoint joinPoint, Set ignoreTypes) { + if (ignoreTypes.contains(IgnoreRequestLog.IgnoreType.REQUEST)) { + return "IGNORED"; + } + String jsonString = JSONObject.toJSONString(this.getUserArgs(joinPoint.getArgs())); + return StringUtils.left(jsonString, MAX_LOG_SIZE); + } + + private String buildResponseLog(Object proceed, Set ignoreTypes) { + if (proceed == null || !(Result.class.isAssignableFrom(proceed.getClass()))) { + return StringUtils.EMPTY; + }else{ + Result r = (Result) proceed; + ((Result) proceed).setTraceId(LogUtils.getTraceId()); + } + if (ignoreTypes.contains(IgnoreRequestLog.IgnoreType.RESPONSE)) { + return "IGNORED"; + } + return StringUtils.left(JSONObject.toJSONString(proceed), MAX_LOG_SIZE); + } + + protected List getUserArgs(Object[] args) { + return Arrays.stream(args) + .filter((p) -> + Objects.nonNull(p) && EXCLUDE_CLASSES.stream().noneMatch(clz -> clz.isAssignableFrom(p.getClass())) + + ).collect(Collectors.toList()); + } +} + diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/log/TraceInterceptor.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/log/TraceInterceptor.java new file mode 100644 index 0000000..78705bb --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/log/TraceInterceptor.java @@ -0,0 +1,61 @@ +package com.sonic.common.log; + +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.sonic.common.AppRuntime; +import org.springframework.util.CollectionUtils; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import com.google.common.collect.Maps; + +import lombok.extern.slf4j.Slf4j; + +/** + * @author code + */ +@Slf4j +public class TraceInterceptor implements HandlerInterceptor { + + private AppRuntime appRuntime; + + public static final String TRACE_VERBOSE_PARAM = "__print_verbose"; + + public TraceInterceptor(AppRuntime appRuntime) { + this.appRuntime = appRuntime; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (CollectionUtils.contains(request.getParameterNames(), TRACE_VERBOSE_PARAM)) { + Map headers = Maps.newHashMap(); + Enumeration headerNames = request.getHeaderNames(); + while (Objects.nonNull(headerNames) && headerNames.hasMoreElements()) { + String name = headerNames.nextElement(); + headers.put(name, request.getHeader(name)); + } + log.info("request url = {}, params = {}, headers = {}, appRuntime = {}", + request.getRequestURI(), request.getParameterMap(), headers, appRuntime); + } + + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + if (CollectionUtils.contains(request.getParameterNames(), TRACE_VERBOSE_PARAM)) { + Map> headers = response.getHeaderNames().stream().collect(Collectors.toMap(h -> h, + h -> Optional.ofNullable(response.getHeaders(h)).orElse(Collections.emptyList()))); + log.info("response headers = {}, status = {}, appRuntime = {}", headers, response.getStatus(), appRuntime); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/RateLimiter.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/RateLimiter.java new file mode 100644 index 0000000..337e5b2 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/RateLimiter.java @@ -0,0 +1,115 @@ +package com.sonic.common.ratelimit; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.ToString; + +public interface RateLimiter { + /** + * 尝试获得锁, 获取失败则返回Optional.empty() + * 如果获取锁成功. 则返回Optional. 同时计数器增加 + * Permit支持取消 + * + * @param value 业务标识 + * @return + */ + Optional tryAcquire(Object value); + + /** + * 获取窗口类型 + * + * @return + */ + WindowType getWindowType(); + + class Permit { + private List cancelRunners; + + @Builder + public Permit(List cancelRunners) { + Objects.requireNonNull(cancelRunners); + this.cancelRunners = cancelRunners; + } + + public void cancel() { + if (!cancelRunners.isEmpty()) { + cancelRunners.stream().forEach(e -> e.run()); + } + } + } + + @Getter + @AllArgsConstructor + enum WindowType { + /** + * 固定窗口, 窗口范围: start = visitTim, end = start + WindowDuration + */ + FIXED("f"), + /** + * 固定窗口, 窗口范围: start = currentMillis/WindowDuration, end = currentMillis/WindowDuration + */ + FIXED_BUCKET("fb"), + /** + * 滑动窗口, 窗口范围: start = currentMillis - WindowDuration, end = currentMillis + */ + SLIDING("s"); + + //减少redisKey长度 + private final String shortName; + } + + /** + * 限流规则 + *
+     *     seconds: 窗口时长
+     *     permits: 允许发放的令牌数量
+     * 
+ */ + @Data + @Builder + @AllArgsConstructor + @ToString + class LimitRule { + long seconds; + int permits; + + public boolean isValid() { + return seconds > 0 && permits > 0; + } + + /** + * 根据约定规则创建Rules + * eg: express 10/1,20/1... 代表10秒1次 & 20秒1次... + * note: seconds不可重复 + */ + public static List fromExpression(String expression) { + if (Strings.isNullOrEmpty(expression)) { + return Collections.emptyList(); + } + Map rulesMap = Splitter.on(",") + .omitEmptyStrings() + .trimResults() + .withKeyValueSeparator("/") + .split(expression); + + return rulesMap.entrySet().stream() + .map(e -> LimitRule.builder() + .seconds(Long.parseLong(e.getKey())) + .permits(Integer.parseInt(e.getValue())) + .build()) + .collect(Collectors.toList()); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/RateLimiterClient.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/RateLimiterClient.java new file mode 100644 index 0000000..a778487 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/RateLimiterClient.java @@ -0,0 +1,43 @@ +package com.sonic.common.ratelimit; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +public interface RateLimiterClient { + + /** + * 构建一个基于Redis的RateLimiter + * + * @return + */ + RateLimiter build(RateLimiterReq rateLimiterReq); + + /** + * 根据windowType与ruleExpression构建一个基于Redis的RateLimiter + * express eg: 10/1,20/1... 代表10秒1次 & 20秒1次... + * + * @param limiterKey + * @param windowType + * @param ruleExpression 规则表达式 + * @return + */ + default RateLimiter build(String limiterKey, RateLimiter.WindowType windowType, String ruleExpression) { + return build(RateLimiterReq.builder() + .windowType(windowType) + .rules(RateLimiter.LimitRule.fromExpression(ruleExpression)) + .limiterKey(limiterKey) + .build()); + } + + @Data + @Builder + @AllArgsConstructor + class RateLimiterReq { + RateLimiter.WindowType windowType; + List rules; + String limiterKey; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/impl/DummyRateLimiter.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/impl/DummyRateLimiter.java new file mode 100644 index 0000000..51dd4c3 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/impl/DummyRateLimiter.java @@ -0,0 +1,30 @@ +package com.sonic.common.ratelimit.impl; + +import java.util.Optional; + +import com.sonic.common.ratelimit.RateLimiter; +import com.sonic.common.ratelimit.RateLimiterClient; +import com.google.common.collect.ImmutableList; + +import lombok.Setter; + +public class DummyRateLimiter implements RateLimiter { + + private RateLimiterClient.RateLimiterReq rateLimiterReq; + @Setter + private boolean result = true; + + public DummyRateLimiter(RateLimiterClient.RateLimiterReq rateLimiterReq) { + this.rateLimiterReq = rateLimiterReq; + } + + @Override + public Optional tryAcquire(Object value) { + return result ? Optional.of(Permit.builder().cancelRunners(ImmutableList.of()).build()) : Optional.empty(); + } + + @Override + public WindowType getWindowType() { + return rateLimiterReq.getWindowType(); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/impl/RateLimiterClientImpl.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/impl/RateLimiterClientImpl.java new file mode 100644 index 0000000..b3677bb --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/impl/RateLimiterClientImpl.java @@ -0,0 +1,69 @@ +package com.sonic.common.ratelimit.impl; + +import java.util.Objects; + +import com.sonic.common.AppRuntime; +import com.sonic.common.enums.AppEnv; +import org.springframework.data.redis.core.RedisTemplate; + +import com.sonic.common.ratelimit.RateLimiter; +import com.sonic.common.ratelimit.RateLimiterClient; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RateLimiterClientImpl implements RateLimiterClient { + @Setter(AccessLevel.PROTECTED) + private AppRuntime appRuntime; + @Setter(AccessLevel.PROTECTED) + private RedisTemplate redisTemplate; + + @Override + public RateLimiter build(RateLimiterReq rateLimiterReq) { + return RedisRateLimiter.builder() + .windowType(rateLimiterReq.getWindowType()) + .limitRules(rateLimiterReq.getRules()) + .limiterKey(rateLimiterReq.getLimiterKey()) + .redisTemplate(redisTemplate) + .appRuntime(appRuntime) + .build(); + } + + public static RateLimiterClientImpl.Builder builder() { + return new RateLimiterClientImpl.Builder(); + } + + @Data + public static class Builder { + + private RedisTemplate redisTemplate; + private AppRuntime appRuntime; + + public RateLimiterClientImpl.Builder redisTemplate(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + return this; + } + + public RateLimiterClientImpl.Builder appRuntime(AppRuntime appRuntime) { + this.appRuntime = appRuntime; + return this; + } + + public RateLimiterClient build() { + if (appRuntime.getEnv() == AppEnv.unittest) { + return DummyRateLimiter::new; + } + //单元测试环境也构建同样的RateLimiterClient + Objects.requireNonNull(redisTemplate); + + RateLimiterClientImpl client = new RateLimiterClientImpl(); + client.setRedisTemplate(redisTemplate); + client.setAppRuntime(appRuntime); + + return client; + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/impl/RedisRateLimiter.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/impl/RedisRateLimiter.java new file mode 100644 index 0000000..e20a778 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/ratelimit/impl/RedisRateLimiter.java @@ -0,0 +1,223 @@ +package com.sonic.common.ratelimit.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import com.sonic.common.AppRuntime; +import com.sonic.common.ratelimit.RateLimiter; +import org.springframework.data.redis.core.*; + +import com.alibaba.fastjson.JSONObject; +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.hash.Hashing; + +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RedisRateLimiter implements RateLimiter { + private AppRuntime appRuntime; + private RedisTemplate redisTemplate; + private RateLimiterWorker rateLimiterWorker; + /** + * 自定义的key, 避免redisKey冲突. 必填 + */ + private String limiterKey; + private List limitRules; + /** + * 窗口保存最大时长. 主要针对Sliding方式窗口的zSet过期 + */ + private Integer maxWindowDurationHour; + private WindowType windowType; + + @Builder + RedisRateLimiter(AppRuntime appRuntime, + RedisTemplate redisTemplate, + WindowType windowType, + String limiterKey, + List limitRules, + Integer maxWindowDurationHour) { + Objects.requireNonNull(appRuntime); + Objects.requireNonNull(redisTemplate); + Objects.requireNonNull(windowType); + Objects.requireNonNull(limitRules); + Objects.requireNonNull(limiterKey); + Preconditions.checkArgument(!limitRules.isEmpty()); + if (limitRules.stream().anyMatch(p -> !p.isValid())) { + throw new RuntimeException(String.format("invalid rate expression, expression = %s", JSONObject.toJSONString(limitRules))); + } + + this.windowType = windowType; + this.appRuntime = appRuntime; + this.redisTemplate = redisTemplate; + this.limitRules = limitRules; + this.limiterKey = limiterKey; + + this.maxWindowDurationHour = Optional.ofNullable(maxWindowDurationHour).orElse(24); + + this.rateLimiterWorker = buildWorker(windowType); + } + + @Override + public Optional tryAcquire(Object value) { + if (!rateLimiterWorker.tryAcquire(value)) { + return Optional.empty(); + } + List cancelRunners = rateLimiterWorker.visit(value); + return Optional.of(Permit.builder() + .cancelRunners(cancelRunners) + .build()); + } + + @Override + public WindowType getWindowType() { + return windowType; + } + + private RateLimiterWorker buildWorker(WindowType windowType) { + if (windowType == WindowType.FIXED) { + return new FixedWindowRateLimiter(); + } + if (windowType == WindowType.FIXED_BUCKET) { + return new FixedBucketWindowRateLimiter(); + } + if (windowType == WindowType.SLIDING) { + return new SlidingWindowRateLimiter(); + } + throw new RuntimeException(String.format("unsupported window type, window type = %s", windowType)); + } + + private String buildRedisKey(Object value) { + String hash = Hashing.murmur3_128().newHasher() + .putString(limiterKey, Charsets.UTF_8) + .putString(String.valueOf(value), Charsets.UTF_8) + .hash() + .toString(); + return appRuntime.buildPrefixKey("rl", getWindowType().getShortName(), hash); + } + + /** + * 固定窗口限流, 窗口起始时间第一次tryAcquire时时间. 窗口大小为WindowDuration + *
+     *     key = value + WindowDuration.
+     *     在该窗口被访问时, 计数器+1. 窗口持续时长为WindowDuration. 并依赖redis ttl销毁
+     *     窗口被销毁后, 重置计数器
+     * 
+ */ + class FixedWindowRateLimiter implements RateLimiterWorker { + @Override + public List visit(Object value) { + List cancels = new ArrayList<>(limitRules.size()); + //根据时间构建key, 避免hash无法删除 + limitRules.stream() + .forEach(e -> { + String key = buildLimiterKey(value, e); + BoundValueOperations op = redisTemplate.boundValueOps(key); + Long result = op.increment(1); + //第一次访问时设置过期时间, 以确定窗口 + if (result == 1) { + redisTemplate.expire(key, e.getSeconds(), TimeUnit.SECONDS); + } + cancels.add(() -> op.decrement(1)); + }); + return cancels; + } + + @Override + public boolean tryAcquire(Object value) { + boolean anyMatch = limitRules.stream() + .anyMatch(limitRule -> { + String key = buildLimiterKey(value, limitRule); + return limitRule.getPermits() <= Optional.ofNullable(redisTemplate.opsForValue().get(key)) + .map(e -> Long.parseLong(e.toString())) + .orElse(0L); + }); + return !anyMatch; + } + + protected String buildLimiterKey(Object value, LimitRule limitRule) { + return buildRedisKey(value + ":" + limitRule.getSeconds()); + } + } + + /** + * 滑动窗口限流, 每次获取令牌成功时间加入到zset. 后续获取令牌时每次检查zset中WindowDuration中已获取令牌数. 并判断是否可以继续获取令牌 + *
+     *     key = value
+     *     zset value = currentMillis. score = currentMillis
+     *     获取令牌时, 在计算zset中 score = [currentMillis-WindowDuration, currentMillis} 的element数量
+     * 
+ */ + class SlidingWindowRateLimiter implements RateLimiterWorker { + //当zset的element达到一定数量时, 清理该zet. 避免redis内存泄露 + private static final int CLEAN_KEY_THRESHOLD = 1000; + private AtomicLong visitCounter = new AtomicLong(); + + @Override + public List visit(Object value) { + String key = buildRedisKey(value); + long now = System.currentTimeMillis(); + String member = String.valueOf(now); + final BoundZSetOperations op = redisTemplate.boundZSetOps(key); + op.add(member, now); + + redisTemplate.expire(key, maxWindowDurationHour, TimeUnit.HOURS); + if (visitCounter.incrementAndGet() > CLEAN_KEY_THRESHOLD) { + //删除过期的访问记录 + op.removeRangeByScore(0, now - TimeUnit.HOURS.toMillis(maxWindowDurationHour)); + visitCounter.set(0); + } + return ImmutableList.of(() -> op.remove(member)); + } + + @Override + public boolean tryAcquire(Object value) { + String key = buildRedisKey(value); + long now = System.currentTimeMillis(); + + //检查所有的rule, 如果有其中一个失败则跳出 + return !limitRules.stream() + .anyMatch(p -> p.getPermits() <= redisTemplate.opsForZSet().count(key, now - TimeUnit.SECONDS.toMillis(p.getSeconds()), now)); + } + } + + /** + * 固定窗口限流, 窗口根据自然时间向前滚动 + * 继承自FixedWindowRateLimiter, 区别在于构建key的方式不一样 + *
+     *     key = value + currentMillis/WindowDuration.
+     *     currentMillis/WindowDuration会把自然时间分割为长为WindowDuration的片段. 片段有效期为WindowDuration
+     *     获取令牌时在该片段上检查是否有剩余令牌
+     * 
+ */ + class FixedBucketWindowRateLimiter extends FixedWindowRateLimiter implements RateLimiterWorker { + @Override + protected String buildLimiterKey(Object value, LimitRule limitRule) { + return buildRedisKey(value + ":" + System.currentTimeMillis() / TimeUnit.SECONDS.toMillis(limitRule.getSeconds())); + } + } + + interface RateLimiterWorker { + /** + * 尝试获取令牌 + * + * @param value + * @return 如果获取成功则返回true, 失败则为false + */ + boolean tryAcquire(Object value); + + /** + * 获取令牌完成后增加计数器 + * + * @param value + * @return limit key + */ + List visit(Object value); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/ApiResultCode.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/ApiResultCode.java new file mode 100644 index 0000000..d48cf65 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/ApiResultCode.java @@ -0,0 +1,27 @@ +package com.sonic.common.rpc; + +/** + * @author code + */ +public interface ApiResultCode { + /** + * 错误码,8 位。 + * + * @return 错误码. + */ + String getErrorCode(); + + /** + * 错误消息 + * + * @return 错误消息. + */ + String getErrorMsg(); + + /** + * 得到应用Id. + * + * @return 应用id. + */ + String getAppId(); +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/AuthRequestProxy.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/AuthRequestProxy.java new file mode 100644 index 0000000..8015dde --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/AuthRequestProxy.java @@ -0,0 +1,29 @@ +package com.sonic.common.rpc; + +import java.util.Optional; +import java.util.function.Function; + +import org.apache.commons.lang3.StringUtils; + +/** + * 代理rpc请求, 并处理授权的token信息. + */ +public interface AuthRequestProxy { + + /** + * 代理runner发起request请求. 在请求中添加授权的token + * 会自动添加相关的授权处理 + * + * @param runner 被代理的请求. + * @param runner返回的数据, key是token + * @return 失败返回Optional.empty, 成功返回runner中返回的数据 + */ + Optional request(Function runner); + + AuthRequestProxy SIMPLE_PROXY = new AuthRequestProxy() { + @Override + public Optional request(Function runner) { + return Optional.ofNullable(runner.apply(StringUtils.EMPTY)); + } + }; +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/AuthRequestProxyImpl.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/AuthRequestProxyImpl.java new file mode 100644 index 0000000..67db1cc --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/AuthRequestProxyImpl.java @@ -0,0 +1,195 @@ +package com.sonic.common.rpc; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import com.sonic.common.enums.AppEnv; +import com.sonic.common.rpc.exception.RpcNetworkException; +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.exception.TokenExpiredException; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.info.GitProperties; +import org.springframework.context.ApplicationContext; + +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.AppRuntime; +import com.google.common.base.Charsets; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * @author code + */ +@Slf4j +public class AuthRequestProxyImpl implements AuthRequestProxy { + private static final int MAX_RETRY_COUNT = 3; + + private RpcClient rpcClient; + + private TokenReq tokenReq; + + private JSONObject globalTokenReqExt; + + @Builder + public AuthRequestProxyImpl(RpcClient rpcClient, TokenReq tokenReq, ApplicationContext applicationContext, AppRuntime appRuntime) { + Objects.requireNonNull(tokenReq); + + this.rpcClient = Optional.ofNullable(rpcClient) + .orElse(new RpcAuthClientImpl(SIMPLE_PROXY)); + this.tokenReq = tokenReq; + + AppRuntime runtime = Optional.ofNullable(appRuntime) + .orElse(AppRuntime.builder() + .appId(tokenReq.getAppId()) + .env(tokenReq.getAppEnv()) + .applicationContext(applicationContext) + .build()); + this.globalTokenReqExt = new JSONObject() + .fluentPut(TokenReq.EXT_START_TIME_KEY, runtime.getStartTime()) + .fluentPut(TokenReq.EXT_GIT_COMMIT_ID_KEY, Optional.ofNullable(runtime.getGitProperties()) + .map(GitProperties::getShortCommitId).orElse(StringUtils.EMPTY)) + .fluentPutAll(runtime.getExt()); + } + + public AuthRequestProxyImpl(TokenReq tokenReq) { + this(null, tokenReq, null, null); + } + + @Deprecated + public AuthRequestProxyImpl(TokenReq tokenReq, ApplicationContext applicationContext) { + this(null, tokenReq, applicationContext, null); + } + + public AuthRequestProxyImpl(RpcClient rpcClient, TokenReq tokenReq) { + this(rpcClient, tokenReq, null, null); + } + + private LoadingCache tokenCache = CacheBuilder. + newBuilder() + .expireAfterWrite(23L, TimeUnit.HOURS) + .maximumSize(2) + .build(new CacheLoader() { + @Override + public TokenResp load(String key) throws Exception { + return fetchToken(); + } + }); + + private TokenResp fetchToken() { + String result; + try { + result = rpcClient.post(tokenReq.getTokenUrl(), + tokenReq.toTokenReq(globalTokenReqExt), + String.class); + } catch (BizException e) { + return TokenResp.builder() + .exception(e) + .build(); + } catch (Exception e) { + log.error("get token from app-center error, params = {}", tokenReq, e); + ApiResultCode resultCode = e.getClass().isAssignableFrom(RpcNetworkException.class) + ? GlobalResultCode.NETWORK_FAILURE + : GlobalResultCode.SYSTEM_EXCEPTION; + return TokenResp.builder() + .exception(new BizException(resultCode, e)) + .build(); + } + + return TokenResp.builder() + .token(result) + .build(); + } + + private String getToken() { + TokenResp tokenResp = tokenCache.getUnchecked("TOKEN"); + if (tokenResp.exception != null) { + tokenCache.invalidateAll(); + throw tokenResp.exception; + } + return tokenResp.token; + } + + + @Override + public Optional request(Function request) { + for (int i = 0; i < MAX_RETRY_COUNT; i++) { + String token = getToken(); + try { + return Optional.ofNullable(request.apply(token)); + } catch (TokenExpiredException e) { + synchronized (this) { + // 只有token没有发生了变化才去刷新. 避免并发情况下同时清除缓存. + if (token.equals(getToken())) { + tokenCache.invalidateAll(); + } + } + } + } + return Optional.empty(); + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + static class TokenResp { + String token; + BizException exception; + + public TokenResp(String token) { + this.token = token; + } + } + + @Data + @Builder + @AllArgsConstructor + public static class TokenReq { + private String appId; + /** + * 应用的环境 + */ + private AppEnv appEnv; + /** + * 扩展参数 + */ + private String ext; + + private String secret; + + private String tokenUrl; + + public static final String EXT_GIT_COMMIT_ID_KEY = "gci"; + public static final String EXT_START_TIME_KEY = "st"; + + public JSONObject toTokenReq(JSONObject globalTokenReqExt) { + long timestamp = System.currentTimeMillis(); + String mixedExt = Optional.ofNullable(JSONObject.parseObject(ext)).orElse(new JSONObject()) + .fluentPutAll(globalTokenReqExt) + .toJSONString(); + + String signature = BaseEncoding.base64().encode(Hashing.sha256().newHasher() + .putString(appId + appEnv.name() + mixedExt + timestamp + secret, Charsets.UTF_8) + .hash() + .asBytes()); + + return new JSONObject() + .fluentPut("appId", appId) + .fluentPut("appEnv", appEnv.name()) + .fluentPut("ext", mixedExt) + .fluentPut("signature", signature) + .fluentPut("timestamp", timestamp); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/GlobalResultCode.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/GlobalResultCode.java new file mode 100644 index 0000000..45d99f2 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/GlobalResultCode.java @@ -0,0 +1,33 @@ +package com.sonic.common.rpc; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author code + */ +@Getter +@AllArgsConstructor +public enum GlobalResultCode implements ApiResultCode { + + /** + * 前 4 位为服务id,全局 id 为 "0000",errorCode 的区间范围在 0000 -> 9999 + */ + SYSTEM_EXCEPTION("00000000", "Internal error, please try again later"), + INVALID_PARAMS("00000001", "Invalid parameters"), + NETWORK_FAILURE("00000003", "Network failure"), + + EVENT_HANDLED_TIMEOUT("00000009", "事件处理耗时太长"), + ; + + /** 全局app默认是0000 **/ + public static final String GLOBAL_APP_ID = "0000"; + + private String errorCode; + private String errorMsg; + + @Override + public String getAppId() { + return GLOBAL_APP_ID; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/HttpClient.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/HttpClient.java new file mode 100644 index 0000000..f5c27bb --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/HttpClient.java @@ -0,0 +1,179 @@ +package com.sonic.common.rpc; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +import org.springframework.util.CollectionUtils; + +import com.google.common.base.Function; +import com.google.common.net.UrlEscapers; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import okhttp3.Request; + +/** + * Http请求客户端. 支持常用的method, 自定义header和自定义处理器. + * @author code + */ +public interface HttpClient { + + String execute(HttpMethod httpMethod, String url, RequestParams requestParams); + + R execute(HttpMethod httpMethod, String url, RequestParams requestParams, BiFunction>, R> responder); + + default String post(String url, RequestParams requestParams) { + return execute(HttpMethod.POST, url, requestParams); + } + + default String get(String url, RequestParams requestParams) { + return execute(HttpMethod.GET, url, requestParams); + } + + default String put(String url, RequestParams requestParams) { + return execute(HttpMethod.PUT, url, requestParams); + } + + default String delete(String url, RequestParams requestParams) { + return execute(HttpMethod.DELETE, url, requestParams); + } + + default R post(String url, RequestParams requestParams, BiFunction>, R> responder) { + return execute(HttpMethod.POST, url, requestParams, responder); + } + + default R get(String url, RequestParams requestParams, BiFunction>, R> responder) { + return execute(HttpMethod.GET, url, requestParams, responder); + } + + default R put(String url, RequestParams requestParams, BiFunction>, R> responder) { + return execute(HttpMethod.PUT, url, requestParams, responder); + } + + default R delete(String url, RequestParams requestParams, BiFunction>, R> responder) { + return execute(HttpMethod.DELETE, url, requestParams, responder); + } + + @Getter + @AllArgsConstructor + enum HttpMethod { + GET((s, requestParams) -> { + return buildUrl(s, requestParams); + }, (builder, requestParams) -> { + return builder.get(); + }), + POST((s, requestParams) -> s, (builder, requestParams) -> { + return builder.post(requestParams.toRequestBody()); + }), + PUT((s, requestParams) -> s, (builder, requestParams) -> { + return builder.put(requestParams.toRequestBody()); + }), + DELETE((s, requestParams) -> { + return buildUrl(s, requestParams); + }, (builder, requestParams) -> { + return builder.delete(); + }); + + BiFunction urlBuilder; + + BiFunction requestBuilder; + + private static String buildUrl(String url, RequestParams requestParams) { + if (requestParams.getContentType() == RequestParams.ContentType.JSON) { + return url; + } + + Map commonParams = ((RequestParams.FormParams) requestParams) + .getParams(entry -> !(entry.getValue() instanceof byte[])); + if (CollectionUtils.isEmpty(commonParams)) { + return url; + } + + Function escape = UrlEscapers.urlPathSegmentEscaper().asFunction(); + String queryString = commonParams.entrySet() + .stream() + .map(p -> escape.apply(p.getKey() + "=" + p.getValue())) + .collect(Collectors.joining("&")); + return url.contains("?") + ? new StringBuilder().append(url).append("&").append(queryString).toString() + : new StringBuilder().append(url).append("?").append(queryString).toString(); + } + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + class Config { + public final static Config EMPTY = Config.builder().build(); + + private static final Long DEFAULT_CONNECT_TIMEOUT = 60_000L; + private static final Long DEFAULT_READ_TIMEOUT = 10_000L; + private static final Long DEFAULT_WRITE_TIMEOUT = 10_000L; + + private static final Integer DEFAULT_MAX_IDLE_CONNECTIONS = 100; + private static final Integer DEFAULT_KEEP_ALIVE_MINUTES = 1; + private static final Integer DEFAULT_ASYNC_MAX_REQUESTS = 100; + private static final Integer DEFAULT_ASYNC_MAX_REQUESTS_PRE_HOST = 20; + + @Getter(AccessLevel.NONE) + private Long connectTimeOut; + + @Getter(AccessLevel.NONE) + private Long writeTimeOut; + + @Getter(AccessLevel.NONE) + private Long readTimeOut; + + // 线程池最大空闲连接数 默认100 + @Getter(AccessLevel.NONE) + private Integer maxIdleConnections; + + //空闲保持时间:分钟 默认 5分钟 + @Getter(AccessLevel.NONE) + private Integer keepAliveMinutes; + + //异步操作最大QPS + @Getter(AccessLevel.NONE) + private Integer asyncMaxRequests; + + //异步操作同一个目的Host:Port的最大并行数 + @Getter(AccessLevel.NONE) + private Integer asyncMaxRequestsPerHost; + + public Long getConnectTimeOut() { + return Objects.isNull(connectTimeOut) ? DEFAULT_CONNECT_TIMEOUT : connectTimeOut; + } + + public Long getWriteTimeOut() { + return Objects.isNull(writeTimeOut) ? DEFAULT_WRITE_TIMEOUT : writeTimeOut; + } + + public Long getReadTimeOut() { + return Objects.isNull(readTimeOut) ? DEFAULT_READ_TIMEOUT : readTimeOut; + } + + public int getMaxIdleConnections() { + return Objects.isNull(maxIdleConnections) ? DEFAULT_MAX_IDLE_CONNECTIONS : maxIdleConnections; + } + + public int getKeepAliveMinutes() { + return Objects.isNull(keepAliveMinutes) ? DEFAULT_KEEP_ALIVE_MINUTES : keepAliveMinutes; + } + + public int getAsyncMaxRequests() { + return Objects.isNull(asyncMaxRequests) ? DEFAULT_ASYNC_MAX_REQUESTS : asyncMaxRequests; + } + + public int getAsyncMaxRequestsPerHost() { + return Objects.isNull(asyncMaxRequestsPerHost) ? DEFAULT_ASYNC_MAX_REQUESTS_PRE_HOST : asyncMaxRequestsPerHost; + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/OkHttpClientImpl.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/OkHttpClientImpl.java new file mode 100644 index 0000000..b878e0d --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/OkHttpClientImpl.java @@ -0,0 +1,143 @@ +package com.sonic.common.rpc; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.function.Function; + +import com.sonic.common.rpc.exception.RpcNetworkException; +import com.sonic.common.rpc.exception.RpcProtocolException; +import com.sonic.common.rpc.exception.TokenExpiredException; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; + +/** + * @author code + */ +@Slf4j +public class OkHttpClientImpl implements HttpClient { + + private OkHttpClient okHttpClient; + + private Config config; + + private List interceptorList; + + private final static long MAX_RESPONSE_LOG_LENGTH = 10_240L; + + public OkHttpClientImpl() { + initClient(); + } + + public OkHttpClientImpl(Config config) { + this.config = config; + initClient(); + } + + public OkHttpClientImpl(Config config, List interceptorList) { + this.config = config; + this.interceptorList = interceptorList; + initClient(); + } + + @Override + public String execute(HttpMethod httpMethod, String url, RequestParams requestParams) { + Objects.requireNonNull(url); + + Request.Builder builder = new Request.Builder() + .url(httpMethod.getUrlBuilder().apply(url, requestParams)) + .headers(Headers.of(Optional.ofNullable(requestParams.getHeaders()).orElse(Collections.emptyMap()))); + + return execute(httpMethod.getRequestBuilder().apply(builder, requestParams), requestParams, this::getStringResponse); + } + + @Override + public R execute(HttpMethod httpMethod, String url, RequestParams requestParams, BiFunction>, R> responder) { + Objects.requireNonNull(url); + + Request.Builder builder = new Request.Builder() + .url(httpMethod.getUrlBuilder().apply(url, requestParams)) + .headers(Headers.of(Optional.ofNullable(requestParams.getHeaders()).orElse(Collections.emptyMap()))); + + return execute(httpMethod.getRequestBuilder().apply(builder, requestParams), requestParams, response -> applyResponder(response, responder)); + } + + private String getStringResponse(Response response) { + try { + return response.body().string(); + } catch (IOException e) { + log.error("OkHttpClient apply string responder error", e); + throw new RpcNetworkException("OkHttpClient apply string responder error", e); + } + } + + private R applyResponder(Response response, BiFunction>, R> responder) { + try { + return responder.apply(response.body().bytes(), response.headers().toMultimap()); + } catch (IOException e) { + log.error("OkHttpClient apply responder error", e); + throw new RpcNetworkException("OkHttpClient apply responder error", e); + } + } + + private R execute(Request.Builder requestBuilder, RequestParams requestParams, + Function responseProcessor) { + Request request = requestBuilder.build(); + + if (requestParams.getReadTimeoutMillis() != null) { + OkHttpClient.Builder builder = okHttpClient.newBuilder(); + builder.readTimeout(requestParams.getReadTimeoutMillis(), TimeUnit.MILLISECONDS); + } + + try (Response response = okHttpClient.newCall(request).execute()) { + if (requestParams.isLogEnabled()) { + log.info("OkHttpClient request success, url = {}, method = {}, param = {}, resp = {}", + request.url().toString(), request.method(), requestParams, response.peekBody(MAX_RESPONSE_LOG_LENGTH).string()); + } + + if (response.code() == TokenExpiredException.STATUS_CODE) { + log.warn("OkHttpClient token expired, url = {}, method = {}, param = {}", request.url().toString(), request.method(), requestParams); + throw new TokenExpiredException(String.format("OkHttpClient token expired, url = %s, method = %s, param = %s", request.url().toString(), request.method(), requestParams)); + } + + if (response.code() >= 500) { + String body = response.peekBody(MAX_RESPONSE_LOG_LENGTH).string(); + log.error("OkHttpClient server error, url = {}, param = {}, response code = {}, body = {}", + request.url().toString(), requestParams, response.code(), body); + throw new RpcProtocolException(response.code(), body, + String.format("OkHttpClient server error, url =%s, param = %s, code = %s", + request.url().toString(), requestParams, response.code())); + } + + return responseProcessor.apply(response); + } catch (IOException e) { + log.error("OkHttpClient network error, url = {}, method = {}, param = {}", request.url().toString(), request.method(), requestParams, e); + throw new RpcNetworkException(String.format("OkHttpClient network error, url = %s, method = %s, param = %s", request.url().toString(), request.method(), requestParams), e); + } + } + + private void initClient() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + Dispatcher dispatcher = new Dispatcher(); + + config = config == null ? Config.EMPTY : config; + + dispatcher.setMaxRequests(config.getAsyncMaxRequests()); + dispatcher.setMaxRequestsPerHost(config.getAsyncMaxRequestsPerHost()); + builder.connectTimeout(config.getConnectTimeOut(), TimeUnit.MILLISECONDS) + .readTimeout(config.getReadTimeOut(), TimeUnit.MILLISECONDS) + .writeTimeout(config.getWriteTimeOut(), TimeUnit.MILLISECONDS) + .connectionPool(new ConnectionPool(config.getMaxIdleConnections(), config.getKeepAliveMinutes(), TimeUnit.MINUTES)) + .dispatcher(dispatcher); + Optional.ofNullable(interceptorList).orElse(Collections.emptyList()). + forEach(interceptor -> builder.addInterceptor(interceptor)); + + okHttpClient = builder.build(); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/Page.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/Page.java new file mode 100644 index 0000000..4561268 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/Page.java @@ -0,0 +1,243 @@ +package com.sonic.common.rpc; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.annotation.JSONField; +import com.sonic.common.exception.BizException; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.net.UrlEscapers; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author code + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Page { + public static final Integer MAX_PER_PAGE_COUNT = 999; + private static final List QUERY_KEYS = ImmutableList.of("pn", "ps", "sortType", "sortField"); + + /** 页码 */ + @Builder.Default + private Integer pn = 1; + /** 每页显示条数 */ + @Builder.Default + private Integer ps = 10; + /** 数据列表 */ + @Builder.Default + private List datas = new ArrayList<>(); + /** 总数据条数 */ + @Builder.Default + private Integer tc = 0; + /** 排序方式 */ + private SortType sortType; + /** 排序字段 */ + private String sortField; + + @AllArgsConstructor + public enum SortType { + ASC, DESC + } + + public void validate() { + if (Optional.ofNullable(pn).orElse(0) < 1) { + throw new BizException(GlobalResultCode.INVALID_PARAMS.getErrorCode(), "当前页码不能为空或小于1"); + } + int notNullPerPage = Optional.ofNullable(ps).orElse(0); + if (notNullPerPage < 1) { + throw new BizException(GlobalResultCode.INVALID_PARAMS.getErrorCode(), "每页容量不能为空或小于1"); + } + if (notNullPerPage > MAX_PER_PAGE_COUNT) { + throw new BizException(GlobalResultCode.INVALID_PARAMS.getErrorCode(), "每页容量不能大于1000"); + } + } + + public String appendQueryStringToUrl(String url) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(url), "url不能为空"); + Preconditions.checkArgument(pn != null, "当前页码不能为空"); + Preconditions.checkArgument(ps != null, "每页容量不能为空"); + Preconditions.checkArgument(pn > 0, "当前页码不能小于1"); + Preconditions.checkArgument(ps > 0, "每页容量不能小于1"); + Preconditions.checkArgument(ps <= MAX_PER_PAGE_COUNT, "每页容量不能大于1000"); + + JSONObject jsonObject = (JSONObject) JSON.toJSON(this); + if (Strings.isNullOrEmpty(sortField)) { + jsonObject.put("sortType", null); + } else if (sortType == null) { + // 默认排序方式为ASC + jsonObject.put("sortType", SortType.ASC); + } + Function escape = UrlEscapers.urlPathSegmentEscaper().asFunction(); + String queryString = jsonObject.entrySet().stream() + // 仅处理分页查询条件 + .filter(p -> QUERY_KEYS.contains(p.getKey())) + .filter(p -> p.getValue() != null) + .map(p -> escape.apply(p.getKey() + "=" + p.getValue())) + .collect(Collectors.joining("&")); + return url + "?" + queryString; + } + + /** + * 创建一个空结果的页(便于业务上快速返回空页) + */ + public Page buildEmptyResultPage() { + return new Page<>(pn, ps, Collections.emptyList(), 0, sortType, sortField); + } + + /** + * sortType默认为ASC + * + * @return + */ + public SortType getSortType() { + return Optional.ofNullable(sortType).orElse(SortType.ASC); + } + + /** + * 用于判断是否支持sort + * 目前只检查了sortField,如果sortType为null,默认用ASC + * @return boolean + */ + @JSONField(serialize = false) + public boolean hasSort() { + return !Strings.isNullOrEmpty(sortField); + } + + /** + * 将 page 中的 datas 映射成另一个类型 + * + * @param mapper + * @param + * @return + */ + public Page convert(Function mapper) { + Page page = ((Page)this); + if (mapper != null && datas != null) { + List collect = datas.stream().map(mapper).collect(Collectors.toList()); + page.setDatas(collect); + } + return page; + } + + public List drainAll(U req, BiFunction, Page> function) { + List totalData = Lists.newArrayList(); + while (true) { + Page newPage = buildEmptyResultPage(); + + Page result = function.apply(req, newPage); + totalData.addAll(result.getDatas()); + + if (result.getDatas().size() < result.getPs()) { + break; + } + pn += 1; + } + return totalData; + } + + public Page(Integer pn, Integer ps) { + this.pn = pn; + this.ps = ps; + } + + public int startIndex() { + return (getPn() - 1) * getPs(); + } + + public int endIndex() { + return getPn() * getPs(); + } + + public boolean firstPage() { + return getPn() <= 1; + } + + public boolean lastPage() { + return getPn() >= pageCount(); + } + + public int nextPage() { + if (lastPage()) { + return getPn(); + } + return getPn() + 1; + } + + public int previousPage() { + if (firstPage()) { + return 1; + } + return getPn() - 1; + } + + public Integer getPn() { + if (pn == null || pn == 0) { + pn = 1; + } + return pn; + } + + public Integer getPs() { + if (ps == null || ps == 0) { + ps = 10; + } + return ps; + } + + public int pageCount() { + if (tc % getPs() == 0) { + return tc / getPs(); + } else { + return tc / getPs() + 1; + } + } + + public Integer getTc() { + return this.tc; + } + + public void setPn(Integer pageNum) { + this.pn = pageNum; + } + + public void setPs(Integer ps) { + this.ps = ps; + } + + public boolean hasNextPage() { + return getPn() < pageCount(); + } + + public boolean hasPreviousPage() { + return getPn() > 1; + } + + public List getDatas() { + return datas; + } + + public void setDatas(List data) { + this.datas = data; + } + + public void setTc(int total) { + this.tc = total; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RequestParams.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RequestParams.java new file mode 100644 index 0000000..57aaae2 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RequestParams.java @@ -0,0 +1,168 @@ +package com.sonic.common.rpc; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; + +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Maps; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import okhttp3.FormBody; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +/** + * @author code + */ +@NoArgsConstructor +@AllArgsConstructor +public class RequestParams { + + private static final String AUTHORIZATION_HEADER_TOKEN_PREFIX = "Bearer "; + private static final String AUTHORIZATION_HEADER_NAME = "Authorization"; + + @Getter + private Map headers; + + @Getter + private ContentType contentType; + + @Getter + private boolean logEnabled; + + @Getter + private Object data; + + @Getter + private Long readTimeoutMillis; + + @Getter + @AllArgsConstructor + public enum ContentType { + /** content-type */ + JSON(MediaType.parse("application/json")), + FORM(MediaType.parse("application/x-www-form-urlencoded")), + FILE_FORM(MediaType.parse("application/octet-stream")); + + private MediaType mediaType; + } + + @Override + public String toString() { + Object data = contentType == ContentType.FORM ? ((FormParams) this).getParams(entry -> !(entry.getValue() instanceof byte[])) : this.getData(); + Map shortHeaders = Maps.transformEntries(Optional.ofNullable(headers).orElse(Collections.emptyMap()), (k, v) -> { + String value = v; + // Authorization 太长不方便日志, 截断. + if (k.equals("Authorization")) { + value = StringUtils.abbreviate(v, 43); + } + return value; + }); + return "data = " + JSONObject.toJSONString(data) + ", " + + "headers = " + JSONObject.toJSONString(shortHeaders) + ", " + + "contentType =" + contentType; + } + + public RequestBody toRequestBody() { + if (contentType == ContentType.JSON) { + return RequestBody.create(ContentType.JSON.getMediaType(), JSONObject.toJSONString(data)); + } + + RequestParams.FormParams formParams = ((RequestParams.FormParams) this); + if (contentType == ContentType.FORM) { + FormBody.Builder formBody = new FormBody.Builder(); + formParams.getParams().forEach((key, value) -> formBody.add(key, String.valueOf(value))); + return formBody.build(); + } + + Map commonParams = formParams.getParams(entry -> !(entry.getValue() instanceof byte[])); + MultipartBody.Builder multiBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + // 处理普通参数 + commonParams.entrySet().forEach(entry -> multiBodyBuilder.addFormDataPart(entry.getKey(), String.valueOf(entry.getValue()))); + // 处理byte[]参数 + formParams.getParams().entrySet().stream(). + filter(entry -> !commonParams.containsKey(entry.getKey())). + forEach(entry -> multiBodyBuilder.addFormDataPart(entry.getKey(), "ignore", RequestBody.create(ContentType.FILE_FORM.getMediaType(), (byte[]) entry.getValue()))); + return multiBodyBuilder.build(); + } + + public void addAuthorization(String token) { + addHeader(AUTHORIZATION_HEADER_NAME, AUTHORIZATION_HEADER_TOKEN_PREFIX + token); + } + + public void addHeader(String name, String value) { + if (headers == null) { + headers = Maps.newHashMap(); + } + headers.put(name, value); + } + + public void addHeaderIfAbsent(String name, String value) { + if (headers == null) { + headers = Maps.newHashMap(); + } + headers.putIfAbsent(name, value); + } + + @EqualsAndHashCode(callSuper = true) + @Data + public static class FormParams extends RequestParams { + private Map params; + + @Builder + public FormParams(Map headers, boolean logEnable, Map params, Long readTimeoutMillis) { + super(headers, hashFileByte(params) ? ContentType.FILE_FORM : ContentType.FORM, logEnable, params, readTimeoutMillis); + this.params = params; + } + + public Map getParams() { + return getParams(entry -> true); + } + + public Map getParams(Predicate predicate) { + Map map = Optional.ofNullable(params).orElse(Collections.EMPTY_MAP); + return map.entrySet() + .stream() + .filter(p -> Objects.nonNull(p.getValue())) + .filter(predicate) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static boolean hashFileByte(Map params) { + Map map = Optional.ofNullable(params).orElse(Collections.EMPTY_MAP); + return map.entrySet().stream() + .anyMatch(entry -> entry.getValue() instanceof byte[]); + } + + @Override + public String toString() { + return super.toString(); + } + } + + @EqualsAndHashCode(callSuper = true) + @Data + public static class BodyParams extends RequestParams { + @Builder + public BodyParams(Map headers, boolean logEnabled, Object content, Long readTimeoutMillis) { + super(headers, ContentType.JSON, logEnabled, content, readTimeoutMillis); + } + + @Override + public String toString() { + return super.toString(); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/Result.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/Result.java new file mode 100644 index 0000000..d0a0c10 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/Result.java @@ -0,0 +1,91 @@ +package com.sonic.common.rpc; + +import com.alibaba.fastjson.annotation.JSONField; + +import com.sonic.common.utils.LogUtils; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * @author code + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Result { + private T content; + private Status status; + /** 异常错误码, 7位 */ + private String errorCode; + private String errorMsg; + private String traceId; + + @JSONField(serialize = false) + public boolean isSuccess() { + return Status.OK == this.status; + } + + public static Result success() { + return Result.builder() + .status(Status.OK) + .build(); + } + + public static Result success(T content) { + return Result.builder() + .status(Status.OK) + .content(content) + .build(); + } + + public static Result error(ApiResultCode resultCode) { + return Result.builder() + .status(Status.ERROR) + .traceId(LogUtils.getTraceId()) + .content(resultCode.getErrorCode()) + .errorMsg(resultCode.getErrorMsg()) + .build(); + } + + public static Result error(ApiResultCode resultCode, String customErrorMsg) { + return Result.builder() + .status(Status.ERROR) + .errorCode(resultCode.getErrorCode()) + .errorMsg(customErrorMsg) + .traceId(LogUtils.getTraceId()) + .build(); + } + + public static Result error(String errorCode, String errorMsg) { + return Result.builder() + .status(Status.ERROR) + .traceId(LogUtils.getTraceId()) + .errorCode(errorCode) + .errorMsg(errorMsg) + .build(); + } + + @Override + public Result clone() { + return Result.builder() + .status(status) + .content(content) + .errorCode(errorCode) + .errorMsg(errorMsg) + .build(); + } + + @Getter + public static enum Status { + /** 接口返回状态 */ + OK, + ERROR, + ; + } + + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcAuthClientImpl.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcAuthClientImpl.java new file mode 100644 index 0000000..e568dbe --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcAuthClientImpl.java @@ -0,0 +1,92 @@ +package com.sonic.common.rpc; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.sonic.common.exception.BizException; +import org.apache.commons.lang3.StringUtils; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * @author code + */ +@Slf4j +public class RpcAuthClientImpl implements RpcClient { + + protected Supplier> customHeaderSupplier; + + @Getter + private HttpClient httpClient; + + private AuthRequestProxy authRequestProxy; + + public RpcAuthClientImpl(AuthRequestProxy authRequestProxy) { + this(authRequestProxy, HttpClient.Config.EMPTY); + } + + public RpcAuthClientImpl(AuthRequestProxy authRequestProxy, HttpClient.Config config) { + this.authRequestProxy = authRequestProxy; + httpClient = new OkHttpClientImpl(config); + } + + public RpcAuthClientImpl(AuthRequestProxy authRequestProxy, Supplier> requestHeaderSupplier) { + this(authRequestProxy); + customHeaderSupplier = requestHeaderSupplier; + } + + public RpcAuthClientImpl(AuthRequestProxy authRequestProxy, HttpClient.Config config, Supplier> requestHeaderSupplier) { + this(authRequestProxy, config); + customHeaderSupplier = requestHeaderSupplier; + } + + @Override + public T execute(HttpClient.HttpMethod httpMethod, String url, RequestParams requestParams, Function> converter) { + Objects.requireNonNull(url); + + Optional resp = requestBySupplier(requestParams, () -> this.getHttpClient().execute(httpMethod, url, requestParams)); + Result result = converter.apply(resp.orElse(StringUtils.EMPTY)); + if (!result.isSuccess()) { + throw new BizException(result.getErrorCode(), result.getErrorMsg()); + } + return result.getContent(); + } + + @Override + public R execute(HttpClient.HttpMethod httpMethod, String url, RequestParams requestParams, BiFunction>, R> responder) { + Objects.requireNonNull(url); + Optional resp = requestBySupplier(requestParams, () -> this.getHttpClient().execute(httpMethod, url, requestParams, responder)); + return resp.orElseGet(() -> responder.apply(new byte[0], Collections.emptyMap())); + } + + Optional requestBySupplier(RequestParams requestParams, Supplier supplier) { + Objects.requireNonNull(requestParams); + + //XXX 附加默认的header, 以及自定义的header + DEFAULT_HEADER_SUPPLIERS.stream() + .forEach(headerSupplier -> headerSupplier.get().entrySet() + .forEach(headerEntry -> + requestParams.addHeaderIfAbsent(headerEntry.getKey(), headerEntry.getValue()) + )); + + if (customHeaderSupplier != null) { + Map map = customHeaderSupplier.get(); + map.entrySet().forEach(e -> { + requestParams.addHeader(e.getKey(), e.getValue()); + }); + } + + return authRequestProxy.request(token -> { + ///XXX 注意此处有一个副作用 + requestParams.addAuthorization(token); + return supplier.get(); + }); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcClient.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcClient.java new file mode 100644 index 0000000..3e2a57b --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcClient.java @@ -0,0 +1,289 @@ +package com.sonic.common.rpc; + +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.sonic.common.context.HttpRequestContext; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Rpc调用客户端接口 + * @author code + */ +public interface RpcClient { + T execute(HttpClient.HttpMethod httpMethod, String url, RequestParams requestParams, Function> converter); + + R execute(HttpClient.HttpMethod httpMethod, String url, RequestParams requestParams, BiFunction>, R> responder); + + + /** post 相关方法, 默认是使用 body */ + default T post(String url, TypeReference> typeReference, RequestParams requestParams) { + return execute(HttpClient.HttpMethod.POST, url, requestParams, s -> JSONObject.parseObject(s, typeReference)); + } + + default T post(String url, BiFunction>, T> responder, RequestParams requestParams) { + return execute(HttpClient.HttpMethod.POST, url, requestParams, responder); + } + + @Deprecated + /**header头参数签名方式提交请求*/ + default T postFormSign(String url, BiFunction>, T> responder, RequestParams requestParams) { + return execute(HttpClient.HttpMethod.POST, url, requestParams, responder); + } + + default T post(String url, Object content, TypeReference> typeReference) { + Preconditions.checkArgument(!(RequestParams.class.isAssignableFrom(content.getClass()))); + Preconditions.checkArgument(!(TypeReference.class.isAssignableFrom(content.getClass()))); + + RequestParams.BodyParams requestParams = RequestParams.BodyParams.builder().content(content).logEnabled(true).build(); + return post(url, typeReference, requestParams); + } + + @Deprecated + /**header头参数签名方式提交请求*/ + default T postBodySign(String url, Object content, TypeReference> typeReference) { + Preconditions.checkArgument(!(RequestParams.class.isAssignableFrom(content.getClass()))); + Preconditions.checkArgument(!(TypeReference.class.isAssignableFrom(content.getClass()))); + RequestParams.BodyParams requestParams = RequestParams.BodyParams.builder().content(content).logEnabled(true).build(); + return post(url, typeReference, requestParams); + } + + default T postBody(String url, Object content, TypeReference> typeReference) { + Preconditions.checkArgument(!(RequestParams.class.isAssignableFrom(content.getClass()))); + Preconditions.checkArgument(!(TypeReference.class.isAssignableFrom(content.getClass()))); + RequestParams.BodyParams requestParams = RequestParams.BodyParams.builder().content(content).logEnabled(true).build(); + return post(url, typeReference, requestParams); + } + + @Deprecated + /**header头参数签名方式提交请求*/ + default T postFormSign(String url, Map params, TypeReference> typeReference) { + RequestParams.FormParams requestParams = RequestParams.FormParams.builder().params(params).logEnable(true).build(); + return post(url, typeReference, requestParams); + } + + default T postForm(String url, Map params, TypeReference> typeReference) { + RequestParams.FormParams requestParams = RequestParams.FormParams.builder().params(params).logEnable(true).build(); + return post(url, typeReference, requestParams); + } + + @Deprecated + default T post(String url, Object content, Class clz) { + return post(url, content, new TypeReference>(clz) { + }); + } + + @Deprecated + default List postAndGetList(String url, Object content, Class clz) { + return post(url, content, new TypeReference>>(clz) { + }); + } + + + /** + * 直接从一个分页查询的服务的接口返回page.getData这个列表; + * 注:该接口直接返回第一页数据,最大1000条 + * + * @param url + * @param content + * @param clz + * @param + * @return + */ + @Deprecated + default List postAndGetListFromPage(String url, Object content, Class clz) { + Page page = Page.builder().pn(1).ps(Page.MAX_PER_PAGE_COUNT).build(); + return Optional.ofNullable(post(page.appendQueryStringToUrl(url), content, new TypeReference>>(clz) {}) + .getDatas()) + .orElse(Collections.emptyList()); + } + + @Deprecated + default Page postAndGetPage(String url, Object content, Class clz) { + Preconditions.checkArgument(!(Page.class.isAssignableFrom(content.getClass()))); + return post(url, content, new TypeReference>>(clz) { + }); + } + + @Deprecated + default Page postAndGetPage(String path, Page page, Object content, Class clz) { + return postAndGetPage(page.appendQueryStringToUrl(path), content, clz); + } + + /* get 相关方法 */ + default T get(String url, TypeReference> typeReference, RequestParams requestParams) { + return execute(HttpClient.HttpMethod.GET, url, requestParams, s -> JSONObject.parseObject(s, typeReference)); + } + + default R get(String url, BiFunction>, R> responder, RequestParams requestParams) { + return execute(HttpClient.HttpMethod.GET, url, requestParams, responder); + } + + default T get(String url, Map params, TypeReference> typeReference) { + RequestParams.FormParams requestParams = RequestParams.FormParams.builder().params(params).logEnable(true).build(); + return get(url, typeReference, requestParams); + } + + @Deprecated + default T get(String url, Map params, Class clz) { + return get(url, params, new TypeReference>(clz) { + }); + } + + /* put 相关方法 */ + default T put(String url, TypeReference> typeReference, RequestParams requestParams) { + return execute(HttpClient.HttpMethod.PUT, url, requestParams, s -> JSONObject.parseObject(s, typeReference)); + } + + default T put(String url, BiFunction>, T> responder, RequestParams requestParams) { + return execute(HttpClient.HttpMethod.PUT, url, requestParams, responder); + } + + default T put(String url, Object content, TypeReference> typeReference) { + Preconditions.checkArgument(!(RequestParams.class.isAssignableFrom(content.getClass()))); + Preconditions.checkArgument(!(TypeReference.class.isAssignableFrom(content.getClass()))); + + RequestParams.BodyParams requestParams = RequestParams.BodyParams.builder().content(content).logEnabled(true).build(); + return put(url, typeReference, requestParams); + } + + /* delete 相关方法*/ + default T delete(String url, TypeReference> typeReference, RequestParams requestParams) { + return execute(HttpClient.HttpMethod.DELETE, url, requestParams, s -> JSONObject.parseObject(s, typeReference)); + } + + default T delete(String url, BiFunction>, T> responder, RequestParams requestParams) { + return execute(HttpClient.HttpMethod.DELETE, url, requestParams, responder); + } + + default T delete(String url, Map params, TypeReference> typeReference) { + RequestParams.FormParams requestParams = RequestParams.FormParams.builder().params(params).logEnable(true).build(); + return delete(url, typeReference, requestParams); + } + + default Result convert(String body, Class clz) { + return JSONObject.parseObject(body, new TypeReference>(clz) { + }); + } + + /** 将 xg-开头的header复制到请求的下一跳 */ + String XG_HEADER_PREFIX = "xg-"; + + List>> DEFAULT_HEADER_SUPPLIERS = ImmutableList.of(() -> HttpRequestContext.get() + .map(request -> Collections.list(request.getHeaderNames()).stream() + .filter(p -> p.startsWith(XG_HEADER_PREFIX)) + .collect(Collectors.toMap(e -> e, e -> request.getHeader(e), (oldValue, newValue) -> newValue))) + .orElse(Collections.emptyMap()) + ); + + /** + * 使用builder模式来发起请求, 先通过request()获得builder对象. 再构建具体的请求方式 + * eg: String resp = rpcClient.request().url("/my").content(Map.of("key", "value")).clz(String.class).post(); + * + * @return + */ + default RpcRequestBuilder request() { + return new RpcRequestBuilder(this); + } + + class RpcRequestBuilder { + private String url; + private Object content; + private Class clz; + private Page page; + private RpcClient rpcClient; + + public RpcRequestBuilder(RpcClient rpcClient) { + this.rpcClient = rpcClient; + } + + public R post() { + checkArgument(); + Preconditions.checkArgument(content != null, "content不能为空"); + return (R) rpcClient.post(getUrl(), content, clz); + } + + public List postAndGetList() { + Preconditions.checkArgument(content != null, "content不能为空"); + checkArgument(); + return rpcClient.postAndGetList(getUrl(), content, clz); + } + + public List postAndGetListFromPage() { + Preconditions.checkArgument(content != null, "content不能为空"); + checkArgument(); + return rpcClient.postAndGetListFromPage(getUrl(), content, clz); + } + + public Page postAndGetPage() { + Preconditions.checkArgument(content != null, "content不能为空"); + checkArgument(); + return rpcClient.postAndGetPage(getUrl(), content, clz); + } + + public R put() { + Preconditions.checkArgument(content != null, "content不能为空"); + checkArgument(); + return rpcClient.put(getUrl(), content, new TypeReference>(clz) { + }); + } + + public R get(Map params) { + Preconditions.checkArgument(content == null, "content不会生效"); + checkArgument(); + return (R) rpcClient.get(getUrl(), params, clz); + } + + public R delete(Map params) { + Preconditions.checkArgument(content == null, "content不会生效"); + checkArgument(); + return rpcClient.delete(getUrl(), params, new TypeReference>(clz) { + }); + } + + private String getUrl() { + return page != null ? page.appendQueryStringToUrl(url) : url; + } + + private void checkArgument() { + Preconditions.checkArgument(!Strings.isNullOrEmpty(url), "url不能为空"); + Preconditions.checkArgument(rpcClient != null, "rpcClient不能为空"); + Preconditions.checkArgument(clz != null, "clz不能为空"); + if (content != null) { + Preconditions.checkArgument(!(RequestParams.class.isAssignableFrom(content.getClass()))); + Preconditions.checkArgument(!(TypeReference.class.isAssignableFrom(content.getClass()))); + Preconditions.checkArgument(!(Page.class.isAssignableFrom(content.getClass()))); + } + } + + public RpcRequestBuilder url(String url) { + this.url = url; + return this; + } + + public RpcRequestBuilder content(Object content) { + this.content = content; + return this; + } + + public RpcRequestBuilder clz(Class clz) { + this.clz = clz; + return this; + } + + public RpcRequestBuilder page(Page page) { + this.page = page; + return this; + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcClientImpl.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcClientImpl.java new file mode 100644 index 0000000..21eda9f --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcClientImpl.java @@ -0,0 +1,98 @@ +package com.sonic.common.rpc; + +import com.sonic.common.exception.BizException; +import com.sonic.common.utils.LogUtils; +import com.google.common.collect.Lists; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * @author code + */ +@Slf4j +public class RpcClientImpl implements RpcClient { + + protected Supplier> customHeaderSupplier; + + @Getter + private HttpClient httpClient; + + private List uriPathPrefix = Lists.newArrayList("/api/"); + + public RpcClientImpl() { + this(HttpClient.Config.EMPTY); + } + + public RpcClientImpl(HttpClient.Config config) { + httpClient = new OkHttpClientImpl(config); + } + + public RpcClientImpl(HttpClient.Config config, List rootPath) { + httpClient = new OkHttpClientImpl(config); + if(!CollectionUtils.isEmpty(rootPath)) { + uriPathPrefix = rootPath; + } + } + + public RpcClientImpl(Supplier> requestHeaderSupplier) { + this(HttpClient.Config.EMPTY); + customHeaderSupplier = requestHeaderSupplier; + } + + public RpcClientImpl(HttpClient.Config config, Supplier> requestHeaderSupplier) { + this(config); + customHeaderSupplier = requestHeaderSupplier; + } + + + @Override + public T execute(HttpClient.HttpMethod httpMethod, String url, RequestParams requestParams, Function> converter) { + String traceId = LogUtils.getTraceId(); + if(StringUtils.isNotEmpty(traceId)){ + requestParams.addHeader("X-TRACE-ID",traceId); + } + Objects.requireNonNull(url); + + Optional resp = requestBySupplier(requestParams, () -> this.getHttpClient().execute(httpMethod, url, requestParams)); + Result result = converter.apply(resp.orElse(StringUtils.EMPTY)); + if (!result.isSuccess()) { + throw new BizException(result.getErrorCode(), result.getErrorMsg()); + } + return result.getContent(); + } + + @Override + public R execute(HttpClient.HttpMethod httpMethod, String url, RequestParams requestParams, BiFunction>, R> responder) { + String traceId = LogUtils.getTraceId(); + if(StringUtils.isNotEmpty(traceId)){ + requestParams.addHeader("X-TRACE-ID",traceId); + } + Objects.requireNonNull(url); + + Optional resp = requestBySupplier(requestParams, () -> this.getHttpClient().execute(httpMethod, url, requestParams, responder)); + return resp.orElseGet(() -> responder.apply(new byte[0], Collections.emptyMap())); + } + + Optional requestBySupplier(RequestParams requestParams, Supplier supplier) { + Objects.requireNonNull(requestParams); + + //XXX 附加默认的header, 以及自定义的header + DEFAULT_HEADER_SUPPLIERS + .forEach(headerSupplier -> headerSupplier.get().forEach(requestParams::addHeaderIfAbsent)); + + if (customHeaderSupplier != null) { + Map map = customHeaderSupplier.get(); + map.forEach(requestParams::addHeader); + } + + return Optional.ofNullable(supplier.get()); + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcClientWrapper.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcClientWrapper.java new file mode 100644 index 0000000..9567ef2 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/RpcClientWrapper.java @@ -0,0 +1,90 @@ +package com.sonic.common.rpc; + +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.sonic.common.exception.BizException; +import org.apache.commons.lang3.StringUtils; + +import com.google.common.base.Preconditions; + +import lombok.Getter; + +/** + * 一个RpcClient的wrapper. 可以方便的在 RpcClient 和 RpcAuthClient 切换. + * 对于RpcAuthClient还可以在构造的时候传递hostResolver, 统一处理host. + */ +public class RpcClientWrapper implements RpcClient { + private Supplier hostResolver; + @Getter + private volatile RpcClient activeRpcClient; + + private RpcClient rpcAuthClient; + private RpcClient rpcClient; + + @lombok.Builder + public RpcClientWrapper(RpcClient rpcAuthClient, RpcClient rpcClient, + Supplier hostResolver, ClientType defaultClientType) { + Preconditions.checkArgument(rpcAuthClient != null || rpcClient != null, "rpcAuthClient和rpcClient必须有一个填入"); + Preconditions.checkArgument(defaultClientType != null, "clientType必须有值"); + Preconditions.checkArgument(hostResolver != null, "hostResolver必须有值"); + if (rpcAuthClient != null) { + Preconditions.checkArgument(rpcAuthClient instanceof RpcAuthClientImpl, "rpcAuthClient 必须是 RpcAuthClientImpl.class 实现"); + } + if (rpcClient != null) { + Preconditions.checkArgument(rpcClient instanceof RpcClientImpl, "rpcClient 必须是 RpcClientImpl.class 实现"); + } + + this.rpcClient = rpcClient; + this.rpcAuthClient = rpcAuthClient; + this.hostResolver = hostResolver; + activate(defaultClientType); + } + + @Override + public T execute(HttpClient.HttpMethod httpMethod, String path, RequestParams requestParams, Function> converter) { + return activeRpcClient.execute(httpMethod, resolvePath(path), requestParams, converter); + } + + @Override + public R execute(HttpClient.HttpMethod httpMethod, String path, RequestParams requestParams, BiFunction>, R> responder) { + return activeRpcClient.execute(httpMethod, resolvePath(path), requestParams, responder); + } + + protected String resolvePath(String path) { + // 如果hostResolver不为空, 且path没有包含protocol与host. 则尝试将host与path拼接 + if (hostResolver != null && !path.startsWith("http")) { + String host = hostResolver.get(); + if (!path.startsWith("/")) { + path = "/" + path; + } + //构建时去掉多余的 '/' + return (host.endsWith("/") ? StringUtils.removeEnd(host, "/") : host) + path; + + } + return path; + } + + public void activate(ClientType type) { + if (type == ClientType.RPC_CLIENT) { + Preconditions.checkState(rpcClient != null); + activeRpcClient = rpcClient; + } else if (type == ClientType.RPC_AUTH_CLIENT) { + Preconditions.checkState(rpcAuthClient != null); + Preconditions.checkState(hostResolver != null); + activeRpcClient = rpcAuthClient; + } else { + throw new BizException(GlobalResultCode.INVALID_PARAMS.getErrorCode(), "不支持的 ClientType"); + } + } + + public enum ClientType { + /** 服务间访问带 token 认证 */ + RPC_AUTH_CLIENT, + /** 服务间访问不带 token 认证 */ + RPC_CLIENT + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/exception/RpcNetworkException.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/exception/RpcNetworkException.java new file mode 100644 index 0000000..c01dc81 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/exception/RpcNetworkException.java @@ -0,0 +1,12 @@ +package com.sonic.common.rpc.exception; + +/** + * 调用网络错误 + * @author code + */ +public class RpcNetworkException extends RuntimeException { + + public RpcNetworkException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/exception/RpcProtocolException.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/exception/RpcProtocolException.java new file mode 100644 index 0000000..cb2841e --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/exception/RpcProtocolException.java @@ -0,0 +1,21 @@ +package com.sonic.common.rpc.exception; + +import lombok.Getter; + +/** + * 协议层错误. 比如http调用4XX, 5XX + * @author code + */ +@Getter +public class RpcProtocolException extends RuntimeException { + + private Integer code; + + private Object response; + + public RpcProtocolException(Integer code, Object response, String message) { + super(message); + this.code = code; + this.response = response; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/exception/TokenExpiredException.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/exception/TokenExpiredException.java new file mode 100644 index 0000000..f7417bd --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/rpc/exception/TokenExpiredException.java @@ -0,0 +1,15 @@ +package com.sonic.common.rpc.exception; + +/** + * Token过期错误. StatusCode=460 + * @author code + */ +public class TokenExpiredException extends RuntimeException { + + public static final int STATUS_CODE = 460; + + public TokenExpiredException(String message) { + super(message); + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/stat/ApiStatInterceptor.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/stat/ApiStatInterceptor.java new file mode 100644 index 0000000..5a9f3f6 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/stat/ApiStatInterceptor.java @@ -0,0 +1,105 @@ +package com.sonic.common.stat; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.sonic.common.auth.AuthCaller; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.annotation.Order; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import com.sonic.common.auth.AuthInterceptor; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +/** + * api访问统计, 放在其他拦截器之后执行 + * @author code + */ +@Order +public class ApiStatInterceptor implements HandlerInterceptor, ApplicationListener { + + private WebApplicationContext webApplicationContext; + + private BiConsumer touchConsumer; + + private RequestMappingHandlerMapping requestMappingHandlerMapping; + + LoadingCache> mappingCache = CacheBuilder.newBuilder() + .maximumSize(1000) + .expireAfterWrite(1, TimeUnit.DAYS) + .build(new CacheLoader>() { + @Override + public Optional load(HandlerMethod handler) throws Exception { + //spring mvc会对handler进行代理, 因此需要获取其被代理对象. 如果不是代理对象这里则应该是该对象自身 + HandlerMethod originHandlerMethod = handler.getResolvedFromHandlerMethod() == null + ? handler + : handler.getResolvedFromHandlerMethod(); + + //从requestMappingHandlerMapping中找到当前handler的RequestMappingInfo + Optional> mappingInfo = requestMappingHandlerMapping.getHandlerMethods() + .entrySet().stream() + .filter(p -> p.getValue() == originHandlerMethod) + .findFirst(); + + return mappingInfo.map(e -> e.getKey()); + } + }); + + public ApiStatInterceptor(WebApplicationContext webApplicationContext, BiConsumer touchConsumer) { + this.touchConsumer = touchConsumer; + this.webApplicationContext = webApplicationContext; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (handler == null + || !handler.getClass().isAssignableFrom(HandlerMethod.class)) { + return true; + } + + Optional unchecked = mappingCache.getUnchecked((HandlerMethod) handler); + if (!unchecked.isPresent()) { + return true; + } + + String appId = getAppId(request); + //从 RequestMappingInfo 找到匹配的当前 request 的 urlMapping + Objects.requireNonNull(unchecked.get().getPatternsCondition().getMatchingCondition(request)).getPatterns() + .forEach(e -> touchConsumer.accept(e + ":" + appId, 1)); + + return true; + } + + private String getAppId(HttpServletRequest request) { + AuthCaller authCaller = (AuthCaller)request.getAttribute(AuthInterceptor.CALLER_PAYLOAD_KEY); + return Optional.ofNullable(authCaller) + .map(AuthCaller::getAppId) + .orElse("unknown"); + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + RequestMappingHandlerMapping mapping = webApplicationContext.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class); + this.requestMappingHandlerMapping = mapping; + + Map handlerMethods = mapping.getHandlerMethods(); + handlerMethods.entrySet() + .stream() + .map(e -> e.getKey().getPatternsCondition().getPatterns()) + .flatMap(e -> e.stream()) + .forEach(e -> touchConsumer.accept(e, 0)); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/stat/FrequencyAlertInterceptor.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/stat/FrequencyAlertInterceptor.java new file mode 100644 index 0000000..c512d1b --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/stat/FrequencyAlertInterceptor.java @@ -0,0 +1,213 @@ +package com.sonic.common.stat; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.sonic.common.auth.OriginalCaller; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.annotation.Order; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.sonic.common.auth.AuthInterceptor; +import com.google.common.base.Splitter; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Lists; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +/** + * 访问告警, 如果某个接口访问次数超过阈值, 则告警 + *

+ * 配置规则: + * /{path} = {seconds}/{permits}. 表示该路径允许多少秒内访问多少次 + * eg: + * /* = 10/100. 表示所有的请求允许10秒内访问100次 + * /index = 5/10 表示/home请求允许5秒内访问10次 + * note: + * 1. 支持通配符与PathVariable + * 2. seconds, permits不能为负数, permits不能超过1000. 否则会被拦截器修正为 seconds = 1, permits = 1000 + *

+ * @author code + */ +@Slf4j +@Order(200) +public class FrequencyAlertInterceptor implements HandlerInterceptor { + private Map rules; + private BiConsumer alertConsumer; + private final static int MAX_TRACER_SIZE = 1000; + + private AntPathMatcher matcher = new AntPathMatcher(); + /** 每个Ip+Rule访问对应的访问时间队列, key = ip+rule, value = touch queue */ + private static final LoadingCache> touchQueueCache = CacheBuilder.newBuilder() + .maximumSize(1000) + .expireAfterAccess(1, TimeUnit.DAYS) + .build(new CacheLoader>() { + @Override + public ArrayBlockingQueue load(String key) throws Exception { + //队列在最大trace size上做200的冗余, 避免访问记录放到队列时失败 + return new ArrayBlockingQueue<>(MAX_TRACER_SIZE + 200); + } + }); + + public FrequencyAlertInterceptor(Map rules, BiConsumer alertConsumer) { + this.alertConsumer = alertConsumer; + this.rules = Rule.fromExpression(rules); + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (handler == null + || !handler.getClass().isAssignableFrom(HandlerMethod.class) + || alertConsumer == null + || rules.isEmpty()) { + return true; + } + + Optional matchRule = getMatchRule(request.getRequestURI()); + if (!matchRule.isPresent()) { + return true; + } + + //key = ip+handlerMethodMessage + String key = getRemoteIp(request) + ((HandlerMethod) handler).getShortLogMessage(); + long now = System.currentTimeMillis(); + + //拿到当前的queue, 并将当前访问时间加到队列中 + ArrayBlockingQueue touchQueue = touchQueueCache.getUnchecked(key); + Object caller = request.getAttribute(AuthInterceptor.CALLER_PAYLOAD_KEY); + touchQueue.offer(Tracer.builder() + .time(now) + .url(request.getRequestURI()) + .caller((OriginalCaller) caller) + .build()); + + //如果访问次数超过配置, 则将头部第一条拿出检查. 时间是否在规定时间内(当前时间-规则限制时间) + if (touchQueue.size() > matchRule.get().getPermits()) { + Tracer head = touchQueue.poll(); + if (head != null && head.getTime() > now - matchRule.get().getMillis()) { + List tracers = Lists.newArrayListWithCapacity(touchQueue.size()); + touchQueue.drainTo(tracers); + + List sortedTracers = tracers.stream() + .sorted(Comparator.comparingLong(Tracer::getTime)) + .collect(Collectors.toList()); + //XXX 此处为了便于观察, 需要计算每个tracer与前一个tracer的时间差值 + for (int i = sortedTracers.size() - 1; i > 0; i--) { + Tracer tracer = sortedTracers.get(i); + tracer.setMillsToPrevious(tracer.getTime() - sortedTracers.get(i - 1).getTime()); + } + + alertConsumer.accept(request, FrequencyAlert.builder() + .rule(matchRule.get()) + .startMillis(head.getTime()) + .endMillis(now) + .tracers(sortedTracers) + .build()); + } + } + return true; + } + + protected Optional getMatchRule(String requestUrl) { + Comparator comparator = matcher.getPatternComparator(requestUrl); + return rules.keySet().stream() + .filter(pattern -> matcher.match(pattern, requestUrl)) + .min(comparator) + .map(e -> rules.get(e)); + } + + private String getRemoteIp(HttpServletRequest request) { + //查找顺序 代理IP>重定向IP>真是IP头, 如果多次转发只保留第一个 + return Splitter.on(",") + .omitEmptyStrings() + .trimResults() + .splitToList(StringUtils.firstNonBlank(request.getHeader("Cdn-Src-Ip"), + request.getHeader("X-Forwarded-For"), + request.getHeader("X-Real-IP"), + request.getRemoteAddr())) + .stream() + .findFirst() + .orElse(StringUtils.EMPTY); + } + + @Data + @Builder + @AllArgsConstructor + public static class Tracer { + private long time; + private String url; + private OriginalCaller caller; + private Long millsToPrevious; + } + + @Data + @Builder + @AllArgsConstructor + public static class FrequencyAlert { + private Rule rule; + private long startMillis; + private long endMillis; + private List tracers; + } + + + @Getter + @Builder + @AllArgsConstructor + @ToString + public static class Rule { + long millis; + int permits; + String path; + + /** + * 根据约定规则创建Rules + * eg: + * /index 10/1 代表index路径规则, 10秒1次 + * /home 5/2 代表home路径规则, 5秒2次 + * /* 10/2 + * note: path不可重复, 优先使用最匹配的路径规则. millis不能为负数, permits不能超过1000 + */ + public static Map fromExpression(Map ruleMap) { + if (ruleMap == null || ruleMap.isEmpty()) { + log.warn("frequency rule not found, the interceptor will be silence"); + return Collections.emptyMap(); + } + return ruleMap.entrySet().stream() + .map(e -> { + List split = Splitter.on("/") + .omitEmptyStrings() + .trimResults() + .splitToList(e.getValue()); + + Rule build = Rule.builder() + .path(e.getKey()) + .millis(Math.max(TimeUnit.SECONDS.toMillis(Long.parseLong(split.get(0))), 1)) + .permits(Math.min(Integer.parseInt(split.get(1)), MAX_TRACER_SIZE)) + .build(); + return build; + + }).collect(Collectors.toMap(Rule::getPath, Function.identity(), (ov, nv) -> nv)); + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/CloudFrontSignerUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/CloudFrontSignerUtils.java new file mode 100644 index 0000000..7bf6d91 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/CloudFrontSignerUtils.java @@ -0,0 +1,190 @@ +package com.sonic.common.utils; + +import com.amazonaws.services.cloudfront.util.SignerUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.*; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.PrivateKey; +import java.util.Base64; +import java.util.Date; + +/** + * @Author code + * @Description CDN节点URL签名访问 + */ +@Component +public class CloudFrontSignerUtils { + + /** + * 模糊图片的尺寸 + */ + public static final String BLURRY_IMG_468_600 = "?x-oss-process=image/resize,w_468,h_600&Tag=1&v=1"; + public static final String BLURRY_IMG_800_800 = "?x-oss-process=image/resize,w_800,h_800&Tag=1&v=1"; + public static final String BLURRY_ORG_IMG = "?Tag=1&v=1"; + + /** + * 清晰图片的尺寸 + */ + public static final String IMG_468_600 = "?x-oss-process=image/resize,w_468,h_600&v=1"; + public static final String IMG_800_800 = "?x-oss-process=image/resize,w_800,h_800&v=1"; + public static final String ORG_IMG = "?v=1"; + + /** + * 永不过期 + */ + public static final Long EXP_TIME_2037 = 2114227200000L; + + @Value("${cloudFront.keyPairId.url:}") + private String cloudFrontKeyPairId; + + @Value("${cloudFront.privateKey.url:}") + private String cloudFrontPrivateKeyUrl; + + private static PrivateKey privateKey; + + /** + * 获取私钥对象 + * @return + */ + private PrivateKey getPrivateKey() { + return getPrivateKey(cloudFrontPrivateKeyUrl); + } + + /** + * 获取私钥对象 + * @return + */ + private PrivateKey getPrivateKey(String privateKeyPemUrl) { + if(privateKey != null) { + return privateKey; + } + try { + File file = downloadRemoteFile(privateKeyPemUrl); + privateKey = SignerUtils.loadPrivateKey(file); + } catch (Exception e) { + e.printStackTrace(); + } + return privateKey; + } + + /** + * 远程下载私钥pem文件 + * @param remoteUrl + * @return + * @throws IOException + */ + public File downloadRemoteFile(String remoteUrl) throws IOException { + // 创建 URL 对象 + URL url = new URL(remoteUrl); + // 打开 URL 的输入流 + try (InputStream inputStream = url.openStream()) { + // 将输入流数据写入到 File 对象 + File localFile = new File("private_key.pem"); + try (OutputStream outputStream = new FileOutputStream(localFile)) { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + return localFile; + } + } + + /** + * 图片访问URL签名处理 + * @param fileUrl 文件全路径 + * @param expDate 过期时间 + * @return + * @throws InvalidKeyException + */ + private String cannedPolicySigner(String fileUrl, Date expDate) throws InvalidKeyException { + //生成策略 + String policy = SignerUtils.buildCannedPolicy(fileUrl, expDate); + //将策略进行base64签名 + String policyBase64 = base64Encode(policy); + //将策略进行参数签名 + String policySign = SignerUtils.makeBytesUrlSafe(SignerUtils.signWithSha1RSA(policy.getBytes(), getPrivateKey())); + + StringBuffer URL = new StringBuffer(); + URL.append(fileUrl); + URL.append("&Policy=").append(policyBase64.replace('+', '-').replace('=', '_').replace('/', '~')); + URL.append("&Signature=").append(policySign.replace('+', '-').replace('=', '_').replace('/', '~')); + URL.append("&Key-Pair-Id=").append(cloudFrontKeyPairId); + //获取完整签名后的URL访问路径 + return URL.toString(); + } + + + /** + * 图片访问URL签名处理 + * @param fileUrl 文件全路径 + * @param expDate 过期时间 + * @param ip 允许访问的IP地址 + * @return + * @throws InvalidKeyException + */ + private String customPolicySigner(String fileUrl, Date expDate, String ip) throws InvalidKeyException { + //生成策略 + String policy = SignerUtils.buildCustomPolicy(fileUrl, expDate, new Date(), ip + "\\/32"); + //将策略进行base64签名 + String policyBase64 = base64Encode(policy); + //将策略进行参数签名 + String policySign = SignerUtils.makeBytesUrlSafe(SignerUtils.signWithSha1RSA(policy.getBytes(), getPrivateKey())); + + StringBuffer URL = new StringBuffer(); + URL.append(fileUrl); + URL.append("&Policy=").append(policyBase64.replace('+', '-').replace('=', '_').replace('/', '~')); + URL.append("&Signature=").append(policySign.replace('+', '-').replace('=', '_').replace('/', '~')); + URL.append("&Key-Pair-Id=").append(cloudFrontKeyPairId); + //获取完整签名后的URL访问路径 + return URL.toString(); + } + + /** + * 参数签名方法 + * @param fileUrl + * @param expDate + * @param ip + * @return + */ + public String signer(String fileUrl, Date expDate, String ip) throws InvalidKeyException { +// //判断ip地址是否是ipV4的地址 +// if(StringUtils.isNotEmpty(ip) && IpAddressUtils.checkIpV4(ip)) { +// //使用自定义策略进行签名 +// return customPolicySigner(fileUrl, expDate, ip); +// } + //使用默认策略进行签名 + return cannedPolicySigner(fileUrl, expDate); + } + + /** + * 对入参数据进行 base64处理 + * @param input + * @return + */ + public String base64Encode(String input) { + byte[] encodedBytes = Base64.getEncoder().encode(input.getBytes(StandardCharsets.UTF_8)); + return new String(encodedBytes, StandardCharsets.UTF_8); + } + + /** + * 将数据根据规则替换后将数据解析成json字符串 + * @param input + * @return + */ + public String base64Decode(String input) { + // 替换规则 + input = input.replace('-', '+').replace('_', '=').replace('~', '/'); + // 解码 Base64 字符串为字节数组 + byte[] decodedBytes = Base64.getDecoder().decode(input); + // 将字节数组转换为字符串 + String decodedJson = new String(decodedBytes, StandardCharsets.UTF_8); + return decodedJson; + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/DataAssembleHelper.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/DataAssembleHelper.java new file mode 100644 index 0000000..ce1ccfc --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/DataAssembleHelper.java @@ -0,0 +1,107 @@ +package com.sonic.common.utils; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.MapFunction; + +/** + * Helper类方便从集合或者bean对象过滤属性和聚合数据 + * 支持指定jsonpath去聚合数目. 通常使用在BFF的controller层. + * https://github.com/json-path/JsonPath + * @author code + */ +public class DataAssembleHelper { + + /** + * 一个Immutable的jsonarray, 防止内容被修改 + */ + private final static JSONArray EMPTY_JSON_ARRAY = new JSONArray(Collections.emptyList()); + + /** + * 给定一个对象集合, 过滤集合中每个对象的属性. + * @param data 对象集合 + * @param includeFields 仅仅包含的属性名称 + * @return 过滤了对象属性的集合 + */ + public static JSONArray filterCollection(Collection data, Set includeFields) { + if (data == null || data.isEmpty()) { + return EMPTY_JSON_ARRAY; + } + + JSONArray jsonList = (JSONArray)JSONObject.toJSON(data); + List collect = IntStream.range(0, jsonList.size()).mapToObj(i -> jsonList.getJSONObject(i)) + .map(e -> { + return new JSONObject(Maps.filterEntries(e, entry -> includeFields.contains(entry.getKey()))); + }).collect(Collectors.toList()); + return new JSONArray(collect); + } + + /** + * 过滤指定bean对象的 + * @param bean + * @param includeFields + * @return jsonobject, 内容是过滤后的bean对象的属性和属性值. + */ + public static JSONObject filterBean(Object bean, Set includeFields) { + Preconditions.checkArgument(!(bean instanceof Collection)); + + if (bean == null) { + return null; + } + + JSONObject json = (JSONObject)JSONObject.toJSON(bean); + return new JSONObject(Maps.filterEntries(json, entry -> includeFields.contains(entry.getKey()))); + } + + /** + * 对bean对象, 根据指定jsonpath中的节点, 替换成mapper中返回的值 + * @param jsonPath 节点路径. 参看 https://github.com/json-path/JsonPath + * @param bean + * @param mapper + * @return 内容是替换后的bean对象的属性和属性值. + */ + public static JSONObject mapBean(String jsonPath, Object bean, Function mapper) { + Preconditions.checkArgument(!(bean instanceof Collection)); + + if (bean == null) { + return null; + } + + JSONObject json = (JSONObject)JSONObject.toJSON(bean); + MapFunction f = (target, conf) -> { + return mapper.apply(target); + }; + return JsonPath.compile(jsonPath).map(json, f, Configuration.defaultConfiguration()); + } + + /** + * 对bean对象集合, 根据指定jsonpath中的节点, 替换成mapper中返回的值 + * @param jsonPath 节点路径. 参看 https://github.com/json-path/JsonPath + * @param beans + * @param mapper + * @return 内容是替换后的集合列表. + */ + public static JSONArray mapBeans(String jsonPath, Collection beans, Function mapper) { + if (beans == null || beans.isEmpty()) { + return EMPTY_JSON_ARRAY; + } + + JSONArray json = (JSONArray)JSONObject.toJSON(beans); + MapFunction f = (target, conf) -> { + return mapper.apply(target); + }; + return JsonPath.compile(jsonPath).map(json, f, Configuration.defaultConfiguration()); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/DateConvertUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/DateConvertUtils.java new file mode 100644 index 0000000..a326eba --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/DateConvertUtils.java @@ -0,0 +1,576 @@ +package com.sonic.common.utils; + +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; + +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAdjusters; +import java.time.temporal.WeekFields; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; + +/** + * LocalDateTime 时间时区转换统一工具类 + * @Author code + * @Date 2020/8/11 17:30 + * @Version 1.0 + */ +@Slf4j +public class DateConvertUtils { + + /** 系统使用的时区 (默认为 CTT 中国上海时区) yml配置:shortZoneId:system 初始化是在: DefaultWebMvcConfig类中*/ + public static String systemShortZoneId = "CTT"; + + /** 需要转换的时区 (默认转换为 PST美国洛杉矶时区) yml配置:shortZoneId:convert 初始化是在: DefaultWebMvcConfig类中*/ + public static String convertShortZoneId = "PST"; + + /** + * 获取当前系统使用的时区ID + * @return + */ + public static String getSystemZoneId() { + return ZoneId.SHORT_IDS.get(systemShortZoneId); + } + + /** + * 获取需要转换使用的时区ID + * @return + */ + public static String getConvertZoneId() { + return ZoneId.SHORT_IDS.get(convertShortZoneId); + } + + /** + * 获取默认转换时区(convertShortZoneId)后的时间 + * @return + */ + public static LocalDateTime getDefaultZoneTime() { + return LocalDateTime.now(Clock.system(ZoneId.of(getConvertZoneId()))); + } + + /** + * 根据传入的时间转换成指定配置时区(convertShortZoneId)后的时间 + * @param localDateTime + * @return + */ + public static LocalDateTime getZoneTime(LocalDateTime localDateTime) { + ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of(getSystemZoneId())).withZoneSameInstant(ZoneId.of(getConvertZoneId())); + return zonedDateTime.toLocalDateTime(); + } + + /** + * 将指定时间的指定时区转换为目标时区的时间 + * @param localDateTime 时间 + * @param fromZone 指定时区 + * @param toZone 目标时区 + * @return + */ + public static LocalDateTime toZone(LocalDateTime localDateTime, String fromZone, String toZone) { + return toZone(localDateTime, ZoneId.of(fromZone), ZoneId.of(toZone)); + } + + /** + * 将指定时间的指定时区转换为目标时区的时间 + * @param localDateTime 时间 + * @param fromZone 指定时区 + * @param toZone 目标时区 + * @return + */ + public static LocalDateTime toZone(LocalDateTime localDateTime, ZoneId fromZone, ZoneId toZone) { + final ZonedDateTime zonedTime = localDateTime.atZone(fromZone); + final ZonedDateTime converted = zonedTime.withZoneSameInstant(toZone); + return converted.toLocalDateTime(); + } + + /** + * 获取指定配置时区(convertShortZoneId)本周周一的 字符串(yyyy-MM-dd)时间 + * @return + */ + public static String getMondayOfThisWeek() { + LocalDateTime monday = getDefaultZoneTime().with(TemporalAdjusters.previous(DayOfWeek.SUNDAY)).plusDays(1); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = dateTimeFormatter.format(monday); + return date; + } + + /** + * 获取指定配置时区(convertShortZoneId)的周一的 LocalDateTime 时间 + * @return + */ + public static LocalDateTime getMondayOfThisWeekToTime() { + LocalDateTime monday = getDefaultZoneTime().with(TemporalAdjusters.previous(DayOfWeek.MONDAY)); + return LocalDateTime.of(monday.toLocalDate(), LocalTime.MIN); + } + + /** + * 获取指定配置时区(convertShortZoneId)且 指定周的 周一的 LocalDateTime 时间 + * @return + */ + public static LocalDateTime getMondayOfWeekToTime(WeekEnum weekEnum) { + LocalDateTime monday = getDefaultZoneTime().with(TemporalAdjusters.previous(DayOfWeek.SUNDAY)); + return LocalDateTime.of(monday.plusDays(weekEnum.day).toLocalDate(), LocalTime.MIN); + } + + public static void main(String[] args) { + + System.out.println(getZoneCurrentDayMinTime(LocalDateTime.now())); + + System.out.println(getZoneCurrentMonday(LocalDateTime.now())); + +// System.out.println(getMondayOfThisWeekToTime()); + System.out.println(getDefaultZoneTime()); + System.out.println(getMondayOfThisWeek()); + System.out.println(getLastMonday(LocalDateTime.now())); + System.out.println(getLocalDateTimeByDayOfWeek(DayOfWeek.WEDNESDAY)); + System.out.println(getLocalDateTimeByDayOfWeek(DayOfWeek.SATURDAY)); + System.out.println(getLocalDateTimeByDayOfWeek(DayOfWeek.SUNDAY)); + } + + /** + * 获取指定配置时区(convertShortZoneId)当前的年数(注意跨年的问题:已解决) + * @return + */ + public static int getWeekDeductionYear() { + LocalDateTime now = getDefaultZoneTime(); + WeekFields weekFields = WeekFields.of(DayOfWeek.MONDAY,4); + int year = now.get(weekFields.weekBasedYear()); + return year; + } + + /** + * 获取指定配置时区(convertShortZoneId)当前年中的周数(注意跨年的问题:已解决) + * @return + */ + public static int getWeekDeductionYearWeek() { + LocalDateTime now = getDefaultZoneTime(); + WeekFields weekFields = WeekFields.of(DayOfWeek.MONDAY,4); + int week = now.get(weekFields.weekOfWeekBasedYear()); + return week; + } + + /** + * 根据传入的时间不做时区转换进行格式转换(yyyy-MM-dd) + * @param localDateTime + * @return + */ + public static String noTimeZoneChangeFormat(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = dateTimeFormatter.format(localDateTime); + return date; + } + + /** + * 根据传入的时间不做时区转换进行格式转换(yyyy-MM-dd) + * @param localDateTime + * @param pattern + * @return + */ + public static String noTimeZoneChangeFormat(LocalDateTime localDateTime, String pattern) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); + String date = dateTimeFormatter.format(localDateTime); + return date; + } + + /** + * 根据传入的时间不做时区转换进行格式转换(yyyy-MM-dd) + * @param localDate + * @return + */ + public static String noTimeZoneChangeFormat(LocalDate localDate) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = localDate.format(dateTimeFormatter); + return date; + } + + /** + * 根据传入的时间不做时区转换进行格式转换(yyyy-MM-dd) + * @param date + * @return + */ + public static String noTimeZoneChangeFormat(Date date) { + SimpleDateFormat dateTimeFormatter = new SimpleDateFormat("yyyy-MM-dd"); + String result = dateTimeFormatter.format(date); + return result; + } + + /** + * 将传入的时间 转换成指定配置时区(convertShortZoneId)格式的字符数据 + * @param localDateTime + * @param pattern + * @return + */ + public static String formatZone(LocalDateTime localDateTime, String pattern) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); + String date = dateTimeFormatter.format(getZoneTime(localDateTime)); + return date; + } + + /** + * 将传入的时间 转换成指定配置时区(convertShortZoneId)指定格式(yyyy-MM-dd)的字符数据 + * @param localDateTime + * @return + */ + public static String formatZone(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = dateTimeFormatter.format(getZoneTime(localDateTime)); + return date; + } + + /** + * 获取指定配置时区(convertShortZoneId)的当前时间,并转换成指定格式(yyyy-MM-dd)的字符数据 + * @return + */ + public static String format() { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = dateTimeFormatter.format(getDefaultZoneTime()); + return date; + } + + /** + * 将传入的时间转换成 转换成指定格式(yyyy-MM-dd)的字符数据 + * @param localDateTime + * @return + */ + @Deprecated + public static String format(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = dateTimeFormatter.format(localDateTime); + return date; + } + + /** + * 将传入的时间转换成指定配置时区(convertShortZoneId)的 数字时间(yyyyMMdd) + * @param localDateTime + * @return + */ + public static Integer formatZoneToInt(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + String date = dateTimeFormatter.format(getZoneTime(localDateTime)); + return Integer.valueOf(date); + } + + /** + * 将传入的时间转换成 数字时间(yyyyMMdd) + * @param localDateTime + * @return + */ + public static Integer formatToInt(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + String date = dateTimeFormatter.format(localDateTime); + return Integer.valueOf(date); + } + + /** + * 获取指定配置时区(convertShortZoneId)的当前时间的最小时间 + * @return + */ + public static LocalDateTime getZoneDateMinTime(){ + LocalDateTime now = getDefaultZoneTime().with(LocalTime.MIN); + return now; + } + + /** + * 获取指定配置时区(convertShortZoneId)的当前时间的最大时间 + * @return + */ + public static LocalDateTime getZoneDateMaxTime(){ + LocalDateTime now = getDefaultZoneTime().with(LocalTime.MAX); + return now; + } + + /** + * 获取传入当天的时间获取当天在转换时区后的开始时间 + * @param localDateTime 传入配置的时区的时间 + * @return + */ + public static LocalDateTime getZoneCurrentDayMinTime(LocalDateTime localDateTime) { + LocalDateTime with = getZoneTime(localDateTime).with(LocalTime.MIN); + LocalDateTime toZoneTime = toZone(with, ZoneId.SHORT_IDS.get(convertShortZoneId), ZoneId.SHORT_IDS.get(systemShortZoneId)); + return toZoneTime; + } + + /** + * 获取上周 周一的开始时间 + * @param localDateTime 传入配置的时区的时间 + * @return + */ + public static LocalDateTime getLastMonday(LocalDateTime localDateTime) { + LocalDateTime with = localDateTime.toLocalDate().minusWeeks(1L).with(DayOfWeek.MONDAY).atStartOfDay(); + LocalDateTime toZoneTime = toZone(with, ZoneId.SHORT_IDS.get(convertShortZoneId), ZoneId.SHORT_IDS.get(systemShortZoneId)); + return toZoneTime; + } + + + /** + * 获取上周 周一的开始时间 + * @param localDateTime 传入配置的时区的时间 + * @return + */ + public static LocalDateTime getZoneLastMonday(LocalDateTime localDateTime) { + LocalDateTime with = localDateTime.toLocalDate().minusWeeks(1L).with(DayOfWeek.MONDAY).atStartOfDay(); + LocalDateTime toZoneTime = toZone(with, ZoneId.SHORT_IDS.get(convertShortZoneId), ZoneId.SHORT_IDS.get(systemShortZoneId)); + return toZoneTime; + } + + /** + * 获取本周 周一的开始时间 + * @param localDateTime 传入配置的时区的时间 + * @return + */ + public static LocalDateTime getZoneCurrentMonday(LocalDateTime localDateTime) { + LocalDateTime with = localDateTime.toLocalDate().with(DayOfWeek.MONDAY).atStartOfDay(); + LocalDateTime toZoneTime = toZone(with, ZoneId.SHORT_IDS.get(convertShortZoneId), ZoneId.SHORT_IDS.get(systemShortZoneId)); + return toZoneTime; + } + + /** + * 获取本周指定周几的开始时间 + * @param dayOfWeek + * @return + */ + public static LocalDateTime getLocalDateTimeByDayOfWeek(DayOfWeek dayOfWeek){ + LocalDateTime with = getDefaultZoneTime().toLocalDate().with(dayOfWeek).atStartOfDay(); + LocalDateTime toZoneTime = toZone(with, ZoneId.SHORT_IDS.get(convertShortZoneId), ZoneId.SHORT_IDS.get(systemShortZoneId)); + return toZoneTime; + } + + + /** + * 获取上周 周日的结束时间 + * @param localDateTime 传入配置的时区的时间 + * @return + */ + public static LocalDateTime getLastSunday(LocalDateTime localDateTime) { + LocalDateTime with = localDateTime.toLocalDate().minusWeeks(1L).with(DayOfWeek.SUNDAY).plusDays(1).atStartOfDay(); + LocalDateTime toZoneTime = toZone(with, ZoneId.SHORT_IDS.get(convertShortZoneId), ZoneId.SHORT_IDS.get(systemShortZoneId)); + return toZoneTime; + } + + /** + * 获取两个时间相差的秒数 + * @param startTime + * @param endTime + * @return + */ + public static Long getTimeDifference(LocalDateTime startTime, LocalDateTime endTime) { + Duration duration = Duration.between(startTime, endTime); + return duration.getSeconds(); + } + + /** + * 获取指定时间的时间戳(单位:秒) + * @param localDateTime + * @return + */ + public static Long getTimeSeconds(LocalDateTime localDateTime) { + Long seconds = localDateTime.toEpochSecond(ZoneOffset.of("+8")); + return seconds; + } + + /** + * 根据传入时区的两个时间 获取两个日期相差的周数 + * + * @param startDate - 开始时间(需要自己转换时区) + * @param endDate - 结束时间(需要自己转换时区) + * @return + */ + public static int getWeeksBetweenTime(LocalDateTime startDate, LocalDateTime endDate) { + Calendar dateFrom = GregorianCalendar.from(ZonedDateTime.of(startDate, ZoneId.systemDefault())); + //设置日历 + Calendar dateTo = GregorianCalendar.from(ZonedDateTime.of(endDate, ZoneId.systemDefault())); + //获取年份 + int yearFrom = dateFrom.get(Calendar.YEAR); + int yearTo = dateTo.get(Calendar.YEAR); + //获取日期所在年的周数 + int weekOfYearFrom = dateFrom.get(Calendar.WEEK_OF_YEAR); + if (weekOfYearFrom == 1 && dateFrom.get(Calendar.MONTH) == Calendar.DECEMBER) { + //腊月最后几天的周数可能为1 + weekOfYearFrom = getWeeksInYear(yearFrom, true); + } + int weekOfYearTo = dateTo.get(Calendar.WEEK_OF_YEAR); + if (weekOfYearTo == 1 && dateTo.get(Calendar.MONTH) == Calendar.DECEMBER) { + weekOfYearTo = getWeeksInYear(yearTo, true); + } + //同年直接返回 + if (yearFrom == yearTo) { + return weekOfYearTo - weekOfYearFrom; + } + //不同年: + int betweenWeeks = weekOfYearTo; + int betweenYears = yearTo - yearFrom; + for (int i = 1; i <= betweenYears ; i++) { + betweenWeeks += getWeeksInYear(yearFrom+i, false); + } + betweenWeeks -= weekOfYearFrom; + return betweenWeeks; + } + + /** + * 获取 某 年共有几周, + * @param year - 年份 + * @param containLastDays - 腊月最后几天不满一周的,是否算为一周 + * @return + */ + public static int getWeeksInYear(int year, boolean containLastDays){ + Calendar calendar = Calendar.getInstance(); + calendar.set(year, Calendar.DECEMBER,31); + int i = calendar.get(Calendar.WEEK_OF_YEAR); + if(i == 1 && containLastDays){ + i = 53; + } + if(i != 53){ + i = 52; + } + return i; + } + + /** + * 获取传入时间获取传入时间当前所在周的周一的时间 + * @param localDateTime + * @return + */ + public static LocalDateTime getCurrentMonday(LocalDateTime localDateTime) { + return localDateTime.with(TemporalAdjusters.previous(DayOfWeek.SUNDAY)).plusDays(1).with(LocalTime.MIN); + } + + +// /** +// * 获取传入时间的上周周一的日期时间 +// * @param localDateTime +// * @return +// */ +// public static LocalDate getLastMonday(LocalDateTime localDateTime) { +// LocalDate now = localDateTime.toLocalDate(); +// LocalDate todayOfLastWeek = now.minusDays(7); +// LocalDate monday = todayOfLastWeek.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)); +// return monday; +// } + + /** + * 根据配置的时区 和指定周枚举值 获取周的所有日期时间 + * @return + */ + public static List getAllDaysOfWeekByShortZoneId(WeekEnum weekEnum) { + if(weekEnum == null) { + return Lists.newArrayList(); + } + //根据时区来进行判断 如果是美国的时区的话 需要减去一天,其他时区暂时不做扣减,后续需要进行扣减的时间需要在 AllDaysOfTheWeekMinusDaysEnum 枚举中进行配置 + return getAllDaysOfWeek(weekEnum, AllDaysOfTheWeekMinusDaysEnum.getMinusDays(convertShortZoneId)); + } + + /** + * 获取指定周内的所有日期时间 + * @param weekEnum + * @param minusDays + * @return + */ + private static List getAllDaysOfWeek(WeekEnum weekEnum, int minusDays) { + List days = Lists.newArrayList(); + days.add(getDefaultZoneTime().with(TemporalAdjusters.previous(DayOfWeek.SUNDAY)).plusDays(weekEnum.day).plusDays(1).minusDays(minusDays).with(LocalTime.MIN)); + days.add(getDefaultZoneTime().with(TemporalAdjusters.next(DayOfWeek.MONDAY)).plusDays(weekEnum.day).minusDays(6).minusDays(minusDays).with(LocalTime.MIN)); + days.add(getDefaultZoneTime().with(TemporalAdjusters.next(DayOfWeek.MONDAY)).plusDays(weekEnum.day).minusDays(5).minusDays(minusDays).with(LocalTime.MIN)); + days.add(getDefaultZoneTime().with(TemporalAdjusters.next(DayOfWeek.MONDAY)).plusDays(weekEnum.day).minusDays(4).minusDays(minusDays).with(LocalTime.MIN)); + days.add(getDefaultZoneTime().with(TemporalAdjusters.next(DayOfWeek.MONDAY)).plusDays(weekEnum.day).minusDays(3).minusDays(minusDays).with(LocalTime.MIN)); + days.add(getDefaultZoneTime().with(TemporalAdjusters.next(DayOfWeek.MONDAY)).plusDays(weekEnum.day).minusDays(2).minusDays(minusDays).with(LocalTime.MIN)); + days.add(getDefaultZoneTime().with(TemporalAdjusters.next(DayOfWeek.MONDAY)).plusDays(weekEnum.day).minusDays(1).minusDays(minusDays).with(LocalTime.MIN)); + List voList = Lists.newArrayList(); + days.forEach(day -> { + AllDaysOfTheWeekVo vo = new AllDaysOfTheWeekVo(); + vo.setIntDayTime(formatToInt(day)); + vo.setDayTime(day); + voList.add(vo); + }); + return voList; + } + + + /** + * 获取指定年、周数的日期 (参考:https://blog.csdn.net/weixin_44919928/article/details/100008249) + * @param year + * @param num + * @param dayOfWeek(MONDAY TUESDAY) + * @return + */ + public static LocalDate getDateByYearAndWeekNumAndDayOfWeekParam(Integer year, Integer num, DayOfWeek dayOfWeek) { + //周数小于10在前面补个0 + String numStr = num < 10 ? "0" + String.valueOf(num) : String.valueOf(num); + //2019-W01-01获取第一周的周一日期,2019-W02-07获取第二周的周日日期 + String weekDate = String.format("%s-W%s-%s", year, numStr, dayOfWeek.getValue()); + return LocalDate.parse(weekDate, DateTimeFormatter.ISO_WEEK_DATE); + } + + /** + * 周类型枚举 + */ + public enum WeekEnum { + /** + * 上周、本周、下周 + */ + PREVIOUS_WEEK(-6), + THIS_WEEK(1), + NEXT_WEEK(8), + ; + + int day; + + WeekEnum(int day) { + this.day = day; + } + } + + public enum AllDaysOfTheWeekMinusDaysEnum { + /** + * PST 美国时间的周一是 中国时间的周日 + * CTT 中国时间 + * */ + PST(1), + CTT(0), + ; + + int minusDays; + + AllDaysOfTheWeekMinusDaysEnum(int minusDays) { + this.minusDays = minusDays; + } + + public static int getMinusDays(String shortZoneId) { + for (AllDaysOfTheWeekMinusDaysEnum allDaysOfTheWeekMinusDaysEnum : AllDaysOfTheWeekMinusDaysEnum.values()) { + if(allDaysOfTheWeekMinusDaysEnum.name().equals(shortZoneId)) { + return allDaysOfTheWeekMinusDaysEnum.minusDays; + } + } + return 0; + } + } + + /** + * 周的所有日期时间对象 + */ + public static class AllDaysOfTheWeekVo { + /** + * 数字时间 yyyyMMdd + */ + private Integer intDayTime; + private LocalDateTime dayTime; + + public Integer getIntDayTime() { + return intDayTime; + } + + public void setIntDayTime(Integer intDayTime) { + this.intDayTime = intDayTime; + } + + public LocalDateTime getDayTime() { + return dayTime; + } + + public void setDayTime(LocalDateTime dayTime) { + this.dayTime = dayTime; + } + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/DateUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/DateUtils.java new file mode 100644 index 0000000..eff48d2 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/DateUtils.java @@ -0,0 +1,26 @@ +package com.sonic.common.utils; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; + +/** + * @author code + * @date 2020-06-04 + */ +public class DateUtils { + private static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault(); + + public static LocalDateTime toLocalDateTime(Date date) { + Instant instant = date.toInstant(); + return instant.atZone(DEFAULT_ZONE_ID).toLocalDateTime(); + } + + public static Date toDate(LocalDateTime localDateTime) { + ZonedDateTime zdt = localDateTime.atZone(DEFAULT_ZONE_ID); + return Date.from(zdt.toInstant()); + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/FastjsonUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/FastjsonUtils.java new file mode 100644 index 0000000..c497fb9 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/FastjsonUtils.java @@ -0,0 +1,21 @@ +package com.sonic.common.utils; + +import com.alibaba.fastjson.serializer.SerializerFeature; + +/** + * @author code + */ +public class FastjsonUtils { + public final static SerializerFeature[] SERIALIZER_FEATURES = { + SerializerFeature.WriteMapNullValue, + // List字段如果为null,输出为[],而非null + SerializerFeature.WriteNullListAsEmpty, + // 用枚举name()输出 + SerializerFeature.WriteEnumUsingName, + // 类中的Get方法对应的Field是transient + SerializerFeature.SkipTransientField, + // 关闭循环引用检查 + SerializerFeature.DisableCircularReferenceDetect + }; + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/IpAddressUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/IpAddressUtils.java new file mode 100644 index 0000000..e3db53c --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/IpAddressUtils.java @@ -0,0 +1,69 @@ +package com.sonic.common.utils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import javax.servlet.http.HttpServletRequest; + +/** + * 获取地区工具类 + * Created by code on 2020/2/29 + */ +@Slf4j +public class IpAddressUtils { + + public static String getIpAddress(HttpServletRequest request) { + //优先从cloudFlare中获取用户的真实IP地址,获取不到再从代理中获取IP地址 + String cfIp = request.getHeader("Cf-Connecting-IP"); + if(StringUtils.isNotEmpty(cfIp)) { + return cfIp; + } + //从亚马逊获取真实IP地址 + String awsIp = request.getHeader("x-original-forwarded-for"); + if(StringUtils.isNotEmpty(awsIp)) { + return awsIp; + } + + String Xip = request.getHeader("X-Real-IP"); + String ip = request.getHeader("X-Forwarded-For"); + + if (StringUtils.isNotBlank(ip) && !"unKnown".equalsIgnoreCase(ip)) { + //多次反向代理后会有多个ip值,第一个ip才是真实ip + int index = ip.indexOf(","); + if (index != -1) { + return ip.substring(0, index); + } else { + return ip; + } + } + ip = Xip; + if (StringUtils.isNotBlank(ip) && !"unKnown".equalsIgnoreCase(ip)) { + return ip; + } + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + /** + * 获取UserAgent + * @param request + * @return + */ + public static String getUserAgent(HttpServletRequest request) { + return request.getHeader("user-agent"); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/LocaleUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/LocaleUtils.java new file mode 100644 index 0000000..aa257ff --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/LocaleUtils.java @@ -0,0 +1,136 @@ +package com.sonic.common.utils; + +import com.google.common.collect.Lists; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Locale; + +/** + * 国际化语言对象获取工具类 + * @Author code + * @Date 2022/1/6 + * @Version 1.0 + */ +@Component +public class LocaleUtils { + + /**默认语言配置*/ + private static final String DEFAULT_LANGUAGE = "en"; + + /**所有有效的语言配置列表 不配置默认为en 英文*/ + private static List allLang; + + /**默认使用的语言配置列表 不配置默认为en 英文*/ + private static String defaultLang; + + @Value("${language.available:en}") + public void setAllLang(String allLangStr) { + List langList = Lists.newArrayList(allLangStr.split(",")); + allLang = langList; + } + + @Value("${language.default:en}") + public void setDefaultLang(String defaultLangStr) { + defaultLang = defaultLangStr; + } + + /** + * 从当前线程中获取绑定的语言环境,获取到的语言环境不为配置的语言环境的话直接使用默认的语言环境 + * @return + */ + public static Locale getLocale() { + Locale locale = LocaleContextHolder.getLocale(); + return getLocaleAndDefault(locale); + } + + /** + * 获取用户使用的语言 国际化处理 + * @param httpServletRequest + * @return + */ + public static Locale getLocale(HttpServletRequest httpServletRequest) { + Locale locale = httpServletRequest.getLocale(); + if(locale == null) { + return new Locale(getDefaultLang()); + } + return getLocaleAndDefault(locale); + } + + /** + * 获取用户使用的语言 国际化处理 + * @param lang + * @return + */ + public static Locale getLocale(String lang) { + if(StringUtils.isEmpty(lang)) { + return new Locale(getDefaultLang()); + } + return getLocaleAndDefault(lang); + } + + /** + * 获取用户使用的语言 国际化处理 + * @param locale + * @return + */ + public static Locale getLocaleAndDefault(Locale locale) { + if(locale == null || StringUtils.isEmpty(locale.getLanguage())) { + return new Locale(getDefaultLang()); + } + return getAllLang().contains(locale.getLanguage()) ? locale : new Locale(getDefaultLang()); + } + + /** + * 获取用户使用的语言 国际化处理 + * @param lang + * @return + */ + public static Locale getLocaleAndDefault(String lang) { + if(StringUtils.isEmpty(lang)) { + return new Locale(getDefaultLang()); + } + return getAllLang().contains(lang) ? new Locale(lang) : new Locale(getDefaultLang()); + } + + /** + * 根据语言对象获取语言字符 + * @param locale + * @return + */ + public static String getLanguage(Locale locale) { + if(locale == null || StringUtils.isEmpty(locale.getLanguage())) { + return getDefaultLang(); + } + return getAllLang().contains(locale.getLanguage()) ? locale.getLanguage() : getDefaultLang(); + } + + + /** + * 处理包扫描没有扫描到的时候的兜底默认处理 + * @return + */ + public static List getAllLang() { + if(CollectionUtils.isEmpty(allLang)) { + allLang = Lists.newArrayList(DEFAULT_LANGUAGE); + return allLang; + } + return allLang; + } + + /** + * 处理包扫描没有扫描到的时候的兜底默认处理 + * @return + */ + public static String getDefaultLang() { + if(StringUtils.isEmpty(defaultLang)) { + return DEFAULT_LANGUAGE; + } + return defaultLang; + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/LogUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/LogUtils.java new file mode 100644 index 0000000..ad6e2cc --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/LogUtils.java @@ -0,0 +1,48 @@ +package com.sonic.common.utils; + +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.MDC; + +/** + * @description: 日志工具类 + * @author: code + * @create: 2020-02-20 15:26 + **/ +public class LogUtils { + + /** + * 日志跟踪标识 + */ + private static final String TRACE_ID = "TRACE_ID"; + + /** + * 设置日志traceId + */ + public static void setTraceId() { + String traceId = UUID.randomUUID().toString(); + if (StringUtils.isEmpty(MDC.get(TRACE_ID))) { + MDC.put(TRACE_ID, traceId); + } + } + + /** + * 获取日志ID + * @return + */ + public static String getTraceId() { + return MDC.get(TRACE_ID); + } + + /** + * 删除日志traceId + */ + public static void removeTraceId() { + MDC.remove(TRACE_ID); + } + + public static void setTraceId(String internalTraceId) { + MDC.put(TRACE_ID, internalTraceId); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/MessageUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/MessageUtils.java new file mode 100644 index 0000000..35d783c --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/MessageUtils.java @@ -0,0 +1,60 @@ +package com.sonic.common.utils; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Component; + +import java.util.Locale; + +/** + * 国际化工具类 + * @author code + * @description 国际化工具 + */ +@Component +public class MessageUtils { + + private static MessageSource messageSource; + + + @Autowired + public void setMessageSource(MessageSource messageSource) { + MessageUtils.messageSource = messageSource; + } + + /** + * 获取单个国际化翻译值 + */ + public static String get(String msgKey) { + try { + String message = messageSource.getMessage(msgKey, null, LocaleUtils.getLocale()); + return message; + } catch (Exception e) { + e.printStackTrace(); + return msgKey; + } + } + + public static String get(String msgKey, Locale locale) { + try { + String message = messageSource.getMessage(msgKey, null, locale); + return message; + } catch (Exception e) { + return msgKey; + } + } + + public static String getMessage(String code, Locale locale) { + try { + String message = messageSource.getMessage(code, null, locale); + return message; + } catch (Exception e) { + return code; + } + } + + public static String getMessage(String code, Object[] args, Locale locale) { + return messageSource.getMessage(code, args, locale); + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/ObjectUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/ObjectUtils.java new file mode 100644 index 0000000..725ef57 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/ObjectUtils.java @@ -0,0 +1,109 @@ +package com.sonic.common.utils; + + +import java.lang.reflect.Array; +import java.time.temporal.TemporalAccessor; +import java.util.Collection; +import java.util.Date; +import java.util.Map; + +/** + * @author code + * @version 1.0 + */ +public class ObjectUtils { + + /** + * 所有数组元素向上转型 + * + * @param objs 转换前对象数组 + * @param clazz 转换后数组对象类型 + * @param + * @return + */ + @SuppressWarnings("unchecked") + public static T[] cast(Object[] objs, Class clazz) { + int length = objs.length; + if (length == 0) { + return (T[]) new Object[0]; + } + T[] newArr = (T[]) Array.newInstance(clazz, objs.length); + for (int i = 0; i < length; i++) { + newArr[i] = clazz.cast(objs[i]); + } + + return newArr; + } + + //--------------------------------------------------------------------- + // 对象类型判断 + //--------------------------------------------------------------------- + + public static boolean isCollection(Object obj) { + return obj instanceof Collection; + } + + public static boolean isMap(Object obj) { + return obj instanceof Map; + } + + public static boolean isNumber(Object obj) { + return obj instanceof Number; + } + + public static boolean isBoolean(Object obj) { + return obj instanceof Boolean; + } + + public static boolean isEnum(Object obj) { + return obj instanceof Enum; + } + + public static boolean isDate(Object obj) { + return obj instanceof Date || obj instanceof TemporalAccessor; + } + + public static boolean isCharSequence(Object obj) { + return obj instanceof CharSequence; + } + + /** + * 判断对象是否为八大基本类型包装类除外即(boolean, byte, char, short, int, long, float, and double)
+ * + * @param obj + * @return + */ + public static boolean isPrimitive(Object obj) { + return obj != null && obj.getClass().isPrimitive(); + } + + /** + * 判断对象是否为包装类或者非包装类的基本类型 + * + * @param obj + * @return + */ + public static boolean isWrapperOrPrimitive(Object obj) { + return isPrimitive(obj) || isNumber(obj) || isCharSequence(obj) || isBoolean(obj); + } + + /** + * 判断一个对象是否为数组 + * + * @param obj + * @return + */ + public static boolean isArray(Object obj) { + return obj != null && obj.getClass().isArray(); + } + + /** + * 判断一个对象是否为基本类型数组即(int[], long[], boolean[], double[]....) + * + * @param obj + * @return + */ + public static boolean isPrimitiveArray(Object obj) { + return isArray(obj) && obj.getClass().getComponentType().isPrimitive(); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/PlaceholderResolver.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/PlaceholderResolver.java new file mode 100644 index 0000000..1c416a6 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/PlaceholderResolver.java @@ -0,0 +1,164 @@ +package com.sonic.common.utils; + +import java.util.Map; +import java.util.Properties; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * 占位符解析器 + * @author code + * @version 1.0 + */ +public class PlaceholderResolver { + /** + * 默认前缀占位符 + */ + public static final String DEFAULT_PLACEHOLDER_PREFIX = "${"; + + /** + * 默认后缀占位符 + */ + public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}"; + + /** + * 默认单例解析器 + */ + private static PlaceholderResolver defaultResolver = new PlaceholderResolver(); + + /** + * 占位符前缀 + */ + private String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX; + + /** + * 占位符后缀 + */ + private String placeholderSuffix = DEFAULT_PLACEHOLDER_SUFFIX; + + + private PlaceholderResolver(){} + + private PlaceholderResolver(String placeholderPrefix, String placeholderSuffix) { + this.placeholderPrefix = placeholderPrefix; + this.placeholderSuffix = placeholderSuffix; + } + + /** + * 获取默认的占位符解析器,即占位符前缀为"${", 后缀为"}" + * @return + */ + public static PlaceholderResolver getDefaultResolver() { + return defaultResolver; + } + + public static PlaceholderResolver getResolver(String placeholderPrefix, String placeholderSuffix) { + return new PlaceholderResolver(placeholderPrefix, placeholderSuffix); + } + + /** + * 解析带有指定占位符的模板字符串,默认占位符为前缀:${ 后缀:}

+ * 如:template = category:${}:product:${}
+ * values = {"1", "2"}
+ * 返回 category:1:product:2
+ * + * @param content 要解析的带有占位符的模板字符串 + * @param values 按照模板占位符索引位置设置对应的值 + * @return + */ + public String resolve(String content, String... values) { + int start = content.indexOf(this.placeholderPrefix); + if (start == -1) { + return content; + } + //值索引 + int valueIndex = 0; + StringBuilder result = new StringBuilder(content); + while (start != -1) { + int end = result.indexOf(this.placeholderSuffix); + String replaceContent = values[valueIndex++]; + result.replace(start, end + this.placeholderSuffix.length(), replaceContent); + start = result.indexOf(this.placeholderPrefix, start + replaceContent.length()); + } + return result.toString(); + } + + /** + * 解析带有指定占位符的模板字符串,默认占位符为前缀:${ 后缀:}

+ * 如:template = category:${}:product:${}
+ * values = {"1", "2"}
+ * 返回 category:1:product:2
+ * + * @param content 要解析的带有占位符的模板字符串 + * @param values 按照模板占位符索引位置设置对应的值 + * @return + */ + public String resolve(String content, Object[] values) { + return resolve(content, Stream.of(values).map(String::valueOf).toArray(String[]::new)); + } + + /** + * 根据替换规则来替换指定模板中的占位符值 + * @param content 要解析的字符串 + * @param rule 解析规则回调 + * @return + */ + public String resolveByRule(String content, Function rule) { + int start = content.indexOf(this.placeholderPrefix); + if (start == -1) { + return content; + } + StringBuilder result = new StringBuilder(content); + while (start != -1) { + int end = result.indexOf(this.placeholderSuffix, start); + //获取占位符属性值,如${id}, 即获取id + String placeholder = result.substring(start + this.placeholderPrefix.length(), end); + //替换整个占位符内容,即将${id}值替换为替换规则回调中的内容 + String replaceContent = placeholder.trim().isEmpty() ? "" : rule.apply(placeholder); + result.replace(start, end + this.placeholderSuffix.length(), replaceContent); + start = result.indexOf(this.placeholderPrefix, start + replaceContent.length()); + } + return result.toString(); + } + + /** + * 替换模板中占位符内容,占位符的内容即为map key对应的值,key为占位符中的内容。

+ * 如:content = product:${id}:detail:${did}
+ * valueMap = id -> 1; pid -> 2
+ * 经过解析返回 product:1:detail:2
+ * + * @param content 模板内容。 + * @param valueMap 值映射 + * @return 替换完成后的字符串。 + */ + public String resolveByMap(String content, final Map valueMap) { + return resolveByRule(content, placeholderValue -> String.valueOf(valueMap.get(placeholderValue))); + } + + /** + * 根据properties文件替换占位符内容 + * @param content + * @param properties + * @return + */ + public String resolveByProperties(String content, final Properties properties) { + return resolveByRule(content, placeholderValue -> properties.getProperty(placeholderValue)); + } + + /** + * 根据对象中字段路径(即类似js访问对象属性值)替换模板中的占位符

+ * 如 content = product:${id}:detail:${detail.id}
+ * obj = Product.builder().id(1).detail(Detail.builder().id(2).build()).build();
+ * 经过解析返回 product:1:detail:2
+ * + * @param content 要解析的内容 + * @param obj 填充解析内容的对象(如果是基本类型,则所有占位符替换为相同的值) + * @return + */ + public String resolveByObject(String content, final Object obj) { + if (obj instanceof Map) { + return resolveByMap(content, (Map)obj); + } + return resolveByRule(content, placeholderValue -> String.valueOf(ReflectionUtils.getValueByFieldPath(obj, placeholderValue))); + } +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/RedisLock.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/RedisLock.java new file mode 100644 index 0000000..f7c618e --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/RedisLock.java @@ -0,0 +1,215 @@ +package com.sonic.common.utils; + +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import lombok.extern.slf4j.Slf4j; + +/** + * @author code + */ +@Slf4j +public class RedisLock { + + private RedisWrapper redis; + + /** + * 默认超时时间(毫秒) + */ + private static final long DEFAULT_TIME_OUT_MILLIS = 5 * 1000; + private static final Random RANDOM = new Random(); + /** + * 锁的超时时间(豪秒),过期删除 + */ + public static final int EXPIRE_IN_MILLIS = 1 * 60 * 1000; + + private String key; + // 锁状态标志 + private boolean locked = false; + + public interface RedisWrapper { + /** + * 删除key + * + * @param key + */ + void delete(String key); + + /** + * 锁定key. 通常使用SetIfAbsent(); + * + * @param key + * @param value + * @param expireMills + * @return + */ + boolean lock(String key, String value, long expireMills); + } + + /** + * 关闭锁,该方法不建议外部直接使用,
+ * 对于加锁执行的操作,建议直接使用 {@link RedisLock#tryAcquireRun(long, long, java.util.function.Supplier)},会自动执行close操作。 + */ + @Deprecated + public void close() { + if (this.locked) { + this.redis.delete(this.key); + } + } + + /** + * This creates a RedisLock + * + * @param key key + * @param redis 数据源 + */ + public RedisLock(String key, RedisWrapper redis) { + this.key = key + ":lock"; + this.redis = redis; + + } + + /** + * 尝试在timeoutMillis毫秒内获取锁并设置锁的过期时间为expireMillis毫秒,若获取锁成功,则执行supplier的逻辑,并返回supplier执行结果。然后关闭锁
+ *
+     * 锁的释放,由2方面保证:
+     * 1、supplier方法执行完成后,会主动释放锁。
+     * 2、设置锁的过期时间
+     * 
+ * 如果只是单纯的尝试获取锁并执行,无需等待锁,可以将timeoutMillis参数设置为0。 + * + * @param timeoutMillis 等待获取锁的时间 单位毫秒(会在等待时间内不停自旋尝试获取锁。)如果超过该时间还没成功获取到锁,则抛出获取锁失败的BizException + * timeoutMillis=0,则表示只进行一次获取锁的尝试。获取失败,直接抛获取锁失败的异常 + * @param expireMillis 锁的过期时间,保证锁最长的持有时间。(如果主动释放锁失败,会有该参数保证锁成功释放) + * @param supplier 需要执行的方法 + * @param 返回参数类型 + * @return + */ + public T tryAcquireRun(final long timeoutMillis, final long expireMillis, Supplier supplier) { + if (!lock(timeoutMillis, expireMillis)) { + throw new AcquireLockFailException("获取锁失败 " + key); + } + try { + return supplier.get(); + } finally { + close(); + } + } + + /** + * 尝试获取锁,并执行supplier.get()方法,返回结果。
+ * 该方法使用了默认的锁等待时间和过期时间:
+ * 等待锁时间={@link #DEFAULT_TIME_OUT_MILLIS 5秒}
+ * 锁过期时间={@link #EXPIRE_IN_MILLIS 1分钟}
+ * 调用该方法,效果等同于 {@link #tryAcquireRun(long, long, Supplier)} + * -> tryAcquireRun(DEFAULT_TIME_OUT_MILLIS, EXPIRE_IN_MILLIS, supplier); + * + * @param supplier + * @param + * @return + */ + public T tryAcquireRun(Supplier supplier) { + if (!lock()) { + throw new AcquireLockFailException("获取锁失败 " + key); + } + try { + return supplier.get(); + } finally { + close(); + } + } + + /** + * 尝试获取锁,并执行supplier.get()方法,返回结果。
+ * 该方法使用了默认的锁过期时间:
+ * 锁过期时间={@link #EXPIRE_IN_MILLIS 1分钟}
+ * 调用该方法,效果等同于 {@link #tryAcquireRun(long, long, Supplier)} + * -> tryAcquireRun(timeoutMillis, EXPIRE_IN_MILLIS, supplier); + * + * @param supplier + * @param + * @return + */ + public T tryAcquireRun(long timeoutMillis, Supplier supplier) { + if (!lock(timeoutMillis)) { + throw new AcquireLockFailException("获取锁失败 " + key); + } + try { + return supplier.get(); + } finally { + close(); + } + } + + /** + * 加锁 应该以: lock(); try { doSomething(); } finally { close(); } 的方式调用
+ * 外部不建议直接使用该方法,建议使用{@link #tryAcquireRun(long, long, Supplier)}明确指定锁的等待和过期时间 + * + * @param timeoutMillis 超时时间(毫秒) + * @return 成功或失败标志 + */ + @Deprecated + public boolean lock(long timeoutMillis) { + return lock(timeoutMillis, EXPIRE_IN_MILLIS); + } + + /** + * 加锁 应该以: lock(); try { doSomething(); } finally { close(); } 的方式调用
+ * 外部不建议直接使用该方法,建议使用{@link #tryAcquireRun(long, long, Supplier)}明确指定锁的等待和过期时间 + * + * @param timeoutMillis 超时时间(毫秒 + * @param expireMillis 锁的超时时间(毫秒),过期删除 + * @return 成功或失败标志 + */ + @Deprecated + public boolean lock(final long timeoutMillis, final long expireMillis) { + long nano = System.nanoTime(); + long timeoutNano = TimeUnit.MILLISECONDS.toNanos(timeoutMillis); + try { + do { + boolean ok = redis.lock(key, "LOCKED", expireMillis); + if (ok) { + this.locked = true; + return this.locked; + } + // 短暂休眠,避免出现活锁 + Thread.sleep(3, RANDOM.nextInt(500)); + } while ((System.nanoTime() - nano) < timeoutNano); + } catch (Exception e) { + throw new AcquireLockFailException("Locking error", e); + } + return false; + } + + /** + * 加锁 应该以: lock(); try { doSomething(); } finally { close(); } 的方式调用
+ * 外部不建议直接使用该方法,建议使用{@link #tryAcquireRun(long, long, Supplier)}明确指定锁的等待和过期时间 + * + * @return 成功或失败标志 + */ + @Deprecated + public boolean lock() { + return lock(DEFAULT_TIME_OUT_MILLIS); + } + + /** 当获取锁失败的时候抛出该异常,方便调用方捕获处理 */ + public static class AcquireLockFailException extends RuntimeException { + public AcquireLockFailException() { + super(); + } + + public AcquireLockFailException(String msg) { + super(msg); + } + + public AcquireLockFailException(Throwable throwable) { + super(throwable); + } + + public AcquireLockFailException(String msg, Throwable throwable) { + super(msg, throwable); + } + } + +} diff --git a/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/ReflectionUtils.java b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/ReflectionUtils.java new file mode 100644 index 0000000..a31a1b8 --- /dev/null +++ b/sonic-common/common-lib-box/common-lib/src/main/java/com/sonic/common/utils/ReflectionUtils.java @@ -0,0 +1,253 @@ +package com.sonic.common.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.springframework.util.Assert; + +/** + * @author code + * @version 1.0 + * @date 2019-03-27 10:04 AM + */ +public final class ReflectionUtils { + + + /** + * 获取所有field字段,包含父类继承的 + * + * @param clazz 字段所属类型 + * @return + */ + public static Field[] getFields(Class clazz) { + return getFields(clazz, null); + } + + /** + * 获取指定类的所有的field,包括父类 + * + * @param clazz 字段所属类型 + * @param fieldFilter 字段过滤器 + * @return 符合过滤器条件的字段数组 + */ + public static Field[] getFields(Class clazz, Predicate fieldFilter) { + List fields = new ArrayList<>(32); + while (Object.class != clazz && clazz != null) { + // 获得该类所有声明的字段,即包括public、private和protected,但是不包括父类的申明字段, + // getFields:获得某个类的所有的公共(public)的字段,包括父类中的字段 + for (Field field : clazz.getDeclaredFields()) { + if (fieldFilter != null && !fieldFilter.test(field)) { + continue; + } + fields.add(field); + } + clazz = clazz.getSuperclass(); + } + return fields.toArray(new Field[0]); + } + + /** + * 对指定类的所有字段执行consumer操作 + * + * @param clazz 目标对象 + * @param consumer 对字段进行操作 + */ + public static void doWithFields(Class clazz, Consumer consumer) { + Arrays.stream(getFields(clazz)).forEach(consumer); + } + + /** + * 获取指定类的指定field,包括父类 + * + * @param clazz 字段所属类型 + * @param name 字段名 + * @return + */ + public static Field getField(Class clazz, String name) { + return getField(clazz, name, null); + } + + /** + * 获取指定类的指定field,包括父类 + * + * @param clazz 字段所属类型 + * @param name 字段名 + * @param type field类型 + * @return Field对象 + */ + public static Field getField(Class clazz, String name, Class type) { + Assert.notNull(clazz, "clazz不能为空!"); + while (clazz != Object.class && clazz != null) { + for (Field field : clazz.getDeclaredFields()) { + if ((name == null || name.equals(field.getName())) && + (type == null || type.equals(field.getType()))) { + return field; + } + } + clazz = clazz.getSuperclass(); + } + return null; + } + + /** + * 获取字段值 + * + * @param field 字段 + * @param target 字段所属实例对象 + * @return 字段值 + */ + public static Object getFieldValue(Field field, Object target) { + makeAccessible(field); + try { + return field.get(target); + } catch (Exception e) { + throw new IllegalStateException(String.format("获取%s对象的%s字段值错误!" + , target.getClass().getName(), field.getName()), e); + } + } + + /** + * 获取对象中指定field值 + * + * @param obj 对象 + * @param fieldName 字段名 + * @return 字段值 + */ + public static Object getFieldValue(Object obj, String fieldName) { + Assert.notNull(obj, "obj不能为空!"); + if (ObjectUtils.isWrapperOrPrimitive(obj)) { + return obj; + } + return getFieldValue(getField(obj.getClass(), fieldName), obj); + } + + /** + * 获取指定对象中指定字段路径的值(类似js访问对象属性)
+ * 如:Product p = new Product(new User())
+ * 可使用ReflectionUtils.getValueByFieldPath(p, "user.name")获取到用户的name属性 + * + * @param obj 取值对象 + * @param fieldPath 字段路径(形如 user.name) + * @return 字段value + */ + public static Object getValueByFieldPath(Object obj, String fieldPath) { + String[] fieldNames = fieldPath.split("\\."); + Object result = null; + for (String fieldName : fieldNames) { + result = getFieldValue(obj, fieldName); + if (result == null) { + return null; + } + obj = result; + } + return result; + } + + /** + * 设置字段值 + * + * @param field 字段 + * @param target 字段所属对象实例 + * @param value 需要设置的值 + */ + public static void setFieldValue(Field field, Object target, Object value) { + makeAccessible(field); + try { + field.set(target, value); + } catch (Exception e) { + throw new IllegalStateException(String.format("设置%s对象的%s字段值错误!" + , target.getClass().getName(), field.getName()), e); + } + } + + /** + * 设置字段为可见 + * + * @param field + */ + public static void makeAccessible(Field field) { + if ((!Modifier.isPublic(field.getModifiers()) || + !Modifier.isPublic(field.getDeclaringClass().getModifiers()) || + Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) { + field.setAccessible(true); + } + } + + /** + * 调用无参数方法 + * + * @param method 方法对象 + * @param target 调用对象 + * @return 执行结果 + */ + public static Object invokeMethod(Method method, Object target) { + return invokeMethod(method, target, new Object[0]); + } + + /** + * 调用指定对象的方法 + * + * @param method 方法对象 + * @param target 调用对象 + * @param args 方法参数 + * @return 执行结果 + */ + public static Object invokeMethod(Method method, Object target, Object... args) { + try { + makeAccessible(method); + return method.invoke(target, args); + } catch (Exception ex) { + throw new IllegalStateException(String.format("执行%s.%s()方法错误!" + , target.getClass().getName(), method.getName()), ex); + } + } + + /** + * 设置方法可见性 + * + * @param method 方法 + */ + public static void makeAccessible(Method method) { + if ((!Modifier.isPublic(method.getModifiers()) || + !Modifier.isPublic(method.getDeclaringClass().getModifiers())) && !method.isAccessible()) { + method.setAccessible(true); + } + } + + /** + * 是否为equals方法 + * + * @see Object#equals(Object) + */ + public static boolean isEqualsMethod(Method method) { + if (!"equals".equals(method.getName())) { + return false; + } + Class[] paramTypes = method.getParameterTypes(); + return (paramTypes.length == 1 && paramTypes[0] == Object.class); + } + + /** + * 是否为hashCode方法 + * + * @see Object#hashCode() + */ + public static boolean isHashCodeMethod(Method method) { + return "hashCode".equals(method.getName()) && method.getParameterCount() == 0; + } + + /** + * 是否为Object的toString方法 + * + * @see Object#toString() + */ + public static boolean isToStringMethod(Method method) { + return "toString".equals(method.getName()) && method.getParameterCount() == 0; + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/pom.xml b/sonic-common/common-lib-box/dao-support-lib/pom.xml new file mode 100644 index 0000000..395d2e3 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/pom.xml @@ -0,0 +1,94 @@ + + + + common-lib-box + com.sonic + 1.0 + + 4.0.0 + + dao-support-lib + 1.0 + jar + + + + com.sonic + common-lib + 1.0.6 + + + + log4j-api + org.apache.logging.log4j + + + + + + mysql + mysql-connector-java + provided + + + com.baomidou + mybatis-plus-boot-starter + provided + + + + log4j-api + org.apache.logging.log4j + + + + + + log4j-api + org.apache.logging.log4j + 2.17.0 + + + + org.apache.commons + commons-lang3 + provided + + + + com.alibaba + fastjson + ${fastjson.version} + + + + com.google.guava + guava + ${guava.version} + + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + com.h2database + h2 + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + + + diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/converter/PageConverter.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/converter/PageConverter.java new file mode 100644 index 0000000..4f4e2c3 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/converter/PageConverter.java @@ -0,0 +1,73 @@ +package com.sonic.daosupport.converter; + +import java.util.function.Function; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.sonic.common.rpc.Page; +import com.google.common.base.CaseFormat; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; + +/** + * 主要用于提供业务Page{@link Page}和
+ * mybatisPlus的查询Page{@link com.baomidou.mybatisplus.extension.plugins.pagination.Page}之间的转换 + * @author code + */ +public class PageConverter { + /** + * 将mybatisPlus的page,转换成业务定义的Page格式,并对当页数据元素进行转换 + * + * @param mpPage + * @param mapper + * @param + * @param + * @return + */ + public static Page convert(IPage mpPage, Function mapper) { + return convert(mpPage.convert(mapper)); + } + + /** + * 将mybatisPlus的page,转换成业务定义的Page格式 + * + * @param mpPage + * @param + * @return + */ + public static Page convert(IPage mpPage) { + Page page = Page.builder() + .datas(mpPage.getRecords()) + .pn((int) mpPage.getCurrent()) + .ps((int) mpPage.getSize()) + .tc((int) mpPage.getTotal()).build(); + // 业务page参数,仅支持对一列进行排序 + mpPage.orders().stream().findFirst().ifPresent(oi -> { + page.setSortField(oi.getColumn()); + page.setSortType(oi.isAsc() ? Page.SortType.ASC : Page.SortType.DESC); + }); + return page; + } + + /** + * 构建一个用于查询的IPage + * + * @param page + * @param + * @return + */ + public static IPage buildQueryPage(Page page) { + com.baomidou.mybatisplus.extension.plugins.pagination.Page mpPage + = new com.baomidou.mybatisplus.extension.plugins.pagination.Page(); + mpPage.setCurrent(page.getPn()); + mpPage.setSize(page.getPs()); + // 处理排序字段 + if (!Strings.isNullOrEmpty(page.getSortField())) { + String columnName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, page.getSortField()); + OrderItem orderItem = page.getSortType() == Page.SortType.ASC ? + OrderItem.asc(columnName) : OrderItem.desc(columnName); + mpPage.setOrders(ImmutableList.of(orderItem)); + } + return mpPage; + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/JsonImportHelper.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/JsonImportHelper.java new file mode 100644 index 0000000..53de9c8 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/JsonImportHelper.java @@ -0,0 +1,240 @@ +package com.sonic.daosupport.mysql; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.util.CollectionUtils; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 一个简单工具类可以从测试环境中通过intellij工具导入数据成json字符串, 然后运行这个脚本导入到线上环境. + * intellij中请使用JSON-groovy格式输出成json + * 注意.导出的数据的字段是数据库的列名需要转换. + *
+ * [
+ *      {
+ *          "id": 2,
+ *          "app_ids": "9999",
+ *          "name": "集成测试告警",
+ *          "subject": "集成测试告警",
+ *          "priority": 98,
+ *          "recipients": "bs-dev@gamers.gg",
+ *          "send_threshold": 1,
+ *          "send_interval": 1,
+ *          "description": "集成测试告警",
+ *          "ext": "",
+ *          "status": "ENABLED",
+ *          "create_at": "2019-10-25 07:10:43",
+ *          "update_at": "2019-11-23 19:51:50"
+ *      }
+ *     ....
+ * ]
+ * 
+ * sample + *
+*         importHelper = JsonImportHelper.builder().baseMapper(alertRuleDao)
+*                 .bizKeyNames(List.of("name"))
+*                 .excludeFields(Set.of())
+*                 .clz(AlertRule.class)
+*                 .saveOrUpdate(null).build();
+ * 
+ * @param + * @param + */ +public class JsonImportHelper, T> { + + private static final Set DEFAULT_EXCLUDE_FIELDS = ImmutableSet.of("", "id", "updatedAt", "createdAt", "updateAt", "createAt"); + private static final String DEFAULT_ID_FIELD_NAME = "id"; + private static final int MAX_IMPORT_COUNT = 100; + + private String idFieldName; + private HashSet excludeFields; + private M baseMapper; + /** + * 业务主键. 比如code, 唯一的名称...能唯一标识这条数据. + */ + private List bizKeyNames; + private Class clz; + private BiConsumer saveOrUpdate; + + /** + * + * @param baseMapper + * @param excludeFields 排除的字段. 比如更新日期,创建日期,id. 默认已经集成. + * @param bizKeyNames + * @param clz + * @param saveOrUpdate + */ + @Builder + public JsonImportHelper(M baseMapper, Set excludeFields, List bizKeyNames, Class clz, + BiConsumer saveOrUpdate, String idFieldName) { + Preconditions.checkArgument(!CollectionUtils.isEmpty(bizKeyNames)); + Preconditions.checkArgument(baseMapper != null); + Preconditions.checkArgument(clz != null); + + this.baseMapper = baseMapper; + this.excludeFields = new HashSet<>(DEFAULT_EXCLUDE_FIELDS); + if (!CollectionUtils.isEmpty(excludeFields)) { + this.excludeFields.addAll(excludeFields); + } + this.bizKeyNames = bizKeyNames; + this.clz = clz; + this.saveOrUpdate = saveOrUpdate; + this.idFieldName = Strings.isNullOrEmpty(idFieldName) ? DEFAULT_ID_FIELD_NAME : idFieldName; + } + + /** + * 执行导入数据操作.返回需要更新, 查询, 没有变化的数据. 如果limit 是0 只是查看数据结果, 不发生操作. + * 一次最多不超过200条数据 + * @param rawJson + * @param limit 指定需要更新的数据数量. 如果是0, 不会更新数据. 只返回结果 + * @return 返回更新数据列表. + */ + public ImportResp run(List rawJson, int limit) { + Preconditions.checkArgument(rawJson.size() <= MAX_IMPORT_COUNT); + List importRows = resolveRows(rawJson); + QueryWrapper query = new QueryWrapper<>(); + + query.last("limit 1000"); + TableInfo tableInfo = TableInfoHelper.getTableInfo(clz); + Map columnMap = tableInfo.getFieldList().stream().collect(Collectors.toMap(e -> e.getColumn(), e -> e.getProperty())); + // 主键没有columnMap中,需要显示声明. + columnMap.put(tableInfo.getKeyColumn(), tableInfo.getKeyProperty()); + // 没有直接使用selectList处理json类型列有问题 + // baseMapper.selectMaps(query) 返回数据是db 列名, 需要转换 + List dbRows = baseMapper.selectMaps(query).stream().map(e->{ + final Map p = e.entrySet().stream() + .collect(Collectors.toMap(entry -> columnMap.get(entry.getKey().toLowerCase()), entry -> entry.getValue())); + return new JSONObject(p); + }).collect(Collectors.toList()); + + ImportResp diffRes = diff(dbRows, importRows); + diffRes.insertRows.stream().limit(limit).forEach(e -> doSaveOrUpdate(e, true)); + diffRes.updateRows.stream().limit(limit).forEach(e -> doSaveOrUpdate(e, false)); + return diffRes; + } + + private void doSaveOrUpdate(T entity, boolean insert) { + if (saveOrUpdate != null) { + saveOrUpdate.accept(entity, insert); + } else { + if (insert) { + baseMapper.insert(entity); + } else { + baseMapper.updateById(entity); + } + } + } + + /** + * 解决数据库的列名到属性名的转换, 并过滤不存在和需要过滤的列 + * @param rows + * @return + */ + List resolveRows(List rows) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(clz); + // 兼容客户端上传的数据是表的column名称(下划线), 或者是熟悉的名称(camel) + ImmutableMap columnMap = Maps.uniqueIndex(tableInfo.getFieldList(), TableFieldInfo::getColumn); + ImmutableMap propertyMap = Maps.uniqueIndex(tableInfo.getFieldList(), TableFieldInfo::getProperty); + Map columnPropertyMap = new HashMap<>(columnMap); + columnPropertyMap.putAll(propertyMap); + + return rows.stream().map(e -> { + Map collect = e.entrySet().stream().map(node -> { + TableFieldInfo column = columnPropertyMap.get(node.getKey()); + if (column == null) { + return Pair.of("", ""); + } + return Pair.of(column.getProperty(), node.getValue()); + }).filter(x -> !excludeFields.contains(x.getKey())) + .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); + return new JSONObject(collect); + }).collect(Collectors.toList()); + } + + ImportResp diff(List dbRows, List importRows) { + // 通过逻辑biz key来关联数据库和导入的数据, 得到需要需要创建和更新的数据. + ImmutableMap dbRowMap = Maps.uniqueIndex(dbRows, e -> { + return bizKeyNames.stream().map(key -> e.getString(key)).collect(Collectors.joining(",")); + }); + + ImmutableMap importRowMap = Maps.uniqueIndex(importRows, e -> { + return bizKeyNames.stream().map(key -> e.getString(key)).collect(Collectors.joining(",")); + }); + ImportResp res = new ImportResp(); + for (Map.Entry entry : importRowMap.entrySet()) { + JSONObject dbRow = dbRowMap.get(entry.getKey()); + JSONObject importRow = entry.getValue(); + if (dbRow != null) { + // 关联成功 + if (isSame(dbRow, importRow)) { + res.sameRows.add(JSONObject.toJavaObject(importRow, clz)); + } else { + // update the id by db row's + JSONObject updated = new JSONObject(importRow); + updated.put(this.idFieldName, dbRow.getLong(this.idFieldName)); + res.updateRows.add(JSONObject.toJavaObject(updated, clz)); + } + } else { + // 没有关联上, 说明需要新增. + res.insertRows.add(JSONObject.toJavaObject(importRow, clz)); + } + } + return res; + } + + /** + * 将json对象串成一个字段串, 来比较2个json是否一致. + * + * @param src + * @param target + * @return + */ + boolean isSame(JSONObject src, JSONObject target) { + Function mixer = i -> { + return i.entrySet().stream().filter(e -> !excludeFields.contains(e.getKey())) + .sorted(Comparator.comparing(Map.Entry::getKey)) + .filter(e -> e.getValue() != null) + .filter(e -> !Strings.isNullOrEmpty(e.getValue().toString())) + .map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(",")); + }; + String srcText = mixer.apply(src); + String targetText = mixer.apply(target); + return srcText.equals(targetText); + } + + @NoArgsConstructor + @AllArgsConstructor + @Data + @Builder + public final static class ImportResp { + List updateRows = Lists.newArrayList(); + List insertRows = Lists.newArrayList(); + List sameRows = Lists.newArrayList(); + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/MybatisPlusConverterUtils.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/MybatisPlusConverterUtils.java new file mode 100644 index 0000000..9f20a37 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/MybatisPlusConverterUtils.java @@ -0,0 +1,39 @@ +package com.sonic.daosupport.mysql; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.sonic.daosupport.wrapper.SimpleWrapperConverter; +import com.google.common.base.Strings; + +/** + * @author code + */ +class MybatisPlusConverterUtils { + + private static Map converters = new ConcurrentHashMap<>(32); + + public static SimpleWrapperConverter getWrapperConverter(Class entityClass) { + return converters.computeIfAbsent(entityClass, clazz -> + SimpleWrapperConverter.builder() + .operatorProcessor(new MybatisPlusOperatorProcessor()) + .fieldColumnMap(getFieldMapping(clazz)) + .build()); + } + + public static Map getFieldMapping(Class clazz) { + // XXX: TableInfoHelper.getTableInfo(clazz).getFieldList返回的映射关系是不包含@TableId注解(会导致根据id查询的列找不到。) + // 在获取property和column映射关系的时候,需要聚合filedList和@TableId + TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz); + Map fieldMap = tableInfo.getFieldList().stream() + .collect(Collectors.toMap(TableFieldInfo::getProperty, TableFieldInfo::getColumn)); + if (!Strings.isNullOrEmpty(tableInfo.getKeyProperty())) { + fieldMap.put(tableInfo.getKeyProperty(), tableInfo.getKeyColumn()); + } + return fieldMap; + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/MybatisPlusOperatorProcessor.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/MybatisPlusOperatorProcessor.java new file mode 100644 index 0000000..103325c --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/MybatisPlusOperatorProcessor.java @@ -0,0 +1,128 @@ +package com.sonic.daosupport.mysql; + +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import com.baomidou.mybatisplus.core.conditions.AbstractWrapper; +import com.baomidou.mybatisplus.core.conditions.interfaces.Compare; +import com.baomidou.mybatisplus.core.conditions.interfaces.Func; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.GlobalResultCode; +import com.sonic.daosupport.wrapper.CriteriaWrapper; +import com.sonic.daosupport.wrapper.Operator; +import com.sonic.daosupport.wrapper.OperatorProcessor; +import com.sonic.daosupport.wrapper.TriConsumer; +import com.google.common.collect.ImmutableListMultimap; + +/** + * @author code + * @param + */ +public class MybatisPlusOperatorProcessor implements OperatorProcessor { + + @Override + public QueryWrapper assembleAllQueryWrapper(ImmutableListMultimap queryColumnMap, boolean andOperator) { + QueryWrapper queryWrapper = Wrappers.query(); + assemble(queryColumnMap, andOperator, queryWrapper); + return queryWrapper; + } + + @Override + public LambdaQueryWrapper assembleAllLambdaQueryWrapper(ImmutableListMultimap queryColumnMap, boolean andOperator) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + assemble(queryColumnMap, andOperator, queryWrapper); + return queryWrapper; + } + + private void assemble(ImmutableListMultimap queryColumnMap, boolean andOperator, AbstractWrapper queryWrapper) { + queryColumnMap.asMap().forEach((column, queryFields) -> { + // 获取processor,拼装wrapper + if (andOperator) { + queryFields.forEach(queryField -> get(queryField.getOperator()) + .accept(queryWrapper, column, queryField.getValue())); + } else { + queryFields.forEach(q -> { + get(q.getOperator()) + .accept(queryWrapper, column, q.getValue()); + queryWrapper.or(); + }); + } + }); + } + + public TriConsumer get(Operator operator) { + switch (operator) { + case LIKE: + return Compare::like; + case EQ: + return Compare::eq; + case LT: + return Compare::lt; + case LE: + return Compare::le; + case GT: + return Compare::gt; + case GE: + return Compare::ge; + case NE: + return Compare::ne; + case IN: + return (TriConsumer) Func::in; + case IS_NULL: + return (TriConsumer) (abstractWrapper, o, o2) -> abstractWrapper.isNull(o); + case IS_NOT_NULL: + return (TriConsumer) (abstractWrapper, o, o2) -> abstractWrapper.isNotNull(o); + case BETWEEN: + return (TriConsumer) (k, v, s) -> { + List valueList = (List)s; + k.between(v, valueList.get(0), valueList.get(1)); + }; + case ORDER: + return (TriConsumer) (k, v, s) -> { + // 如果value不能转成Direction,会抛异常 + if (Direction.fromString(String.valueOf(s)).isDescending()) { + k.orderByDesc(v); + return; + } + + k.orderByAsc(v); + }; + default: + throw new BizException(GlobalResultCode.INVALID_PARAMS); + } + } + + public enum Direction { + /** sort type */ + ASC, + DESC; + + public boolean isAscending() { + return this.equals(ASC); + } + + public boolean isDescending() { + return this.equals(DESC); + } + + public static Direction fromString(String value) { + try { + return valueOf(value.toUpperCase(Locale.US)); + } catch (Exception var2) { + throw new IllegalArgumentException(String.format("Invalid value '%s' for orders given! Has to be either 'desc' or 'asc' (case insensitive).", value), var2); + } + } + + public static Optional fromOptionalString(String value) { + try { + return Optional.of(fromString(value)); + } catch (IllegalArgumentException var2) { + return Optional.empty(); + } + } + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/QueryWrapperHelper.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/QueryWrapperHelper.java new file mode 100644 index 0000000..2060cf3 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/QueryWrapperHelper.java @@ -0,0 +1,34 @@ +package com.sonic.daosupport.mysql; + +import java.util.Map; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.sonic.daosupport.wrapper.CriteriaWrapper; +import com.sonic.daosupport.wrapper.SimpleWrapperConverter; + +/** + * @author code + */ +public class QueryWrapperHelper { + + public static QueryWrapper fromMap(Object bean, Class clazz) { + SimpleWrapperConverter converter = MybatisPlusConverterUtils.getWrapperConverter(clazz); + return converter.toWrapper(CriteriaWrapper.fromBean(bean)); + } +// TODO: 暂时不支持返回 LambdaQueryWrapper +// public static LambdaQueryWrapper getLambdaQueryWrapperFromBean(Object bean, Class clazz) { +// SimpleWrapperConverter converter = MybatisPlusConverterUtils.getWrapperConverter(clazz); +// return converter.toLambdaWrapper(CriteriaWrapper.fromBean(bean)); +// } + + public static QueryWrapper fromMap(Map map, Class clazz) { + SimpleWrapperConverter converter = MybatisPlusConverterUtils.getWrapperConverter(clazz); + return converter.toWrapper(CriteriaWrapper.builder().queryField(map).build()); + } +// TODO: 暂时不支持返回 LambdaQueryWrapper +// public static LambdaQueryWrapper getLambdaQueryWrapperFromMap(Map map, Class clazz) { +// SimpleWrapperConverter converter = MybatisPlusConverterUtils.getWrapperConverter(clazz); +// return converter.toLambdaWrapper(CriteriaWrapper.builder().queryField(map).build()); +// } + +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/typehandler/BaseListTypeHandler.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/typehandler/BaseListTypeHandler.java new file mode 100644 index 0000000..3beaf92 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/typehandler/BaseListTypeHandler.java @@ -0,0 +1,55 @@ +package com.sonic.daosupport.mysql.typehandler; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.serializer.SerializerFeature; + +/** + * @author code + */ +@MappedTypes({List.class}) +@MappedJdbcTypes(JdbcType.VARCHAR) +public abstract class BaseListTypeHandler extends BaseTypeHandler> { + + private Class type = getGenericType(); + + @Override + public void setNonNullParameter(PreparedStatement preparedStatement, int i, + List list, JdbcType jdbcType) throws SQLException { + preparedStatement.setString(i, JSONArray.toJSONString(list, SerializerFeature.WriteMapNullValue, + SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty)); + } + + @Override + public List getNullableResult(ResultSet resultSet, String s) throws SQLException { + return JSONArray.parseArray(resultSet.getString(s), type); + } + + @Override + public List getNullableResult(ResultSet resultSet, int i) throws SQLException { + return JSONArray.parseArray(resultSet.getString(i), type); + } + + @Override + public List getNullableResult(CallableStatement callableStatement, int i) throws SQLException { + return JSONArray.parseArray(callableStatement.getString(i), type); + } + + private Class getGenericType() { + Type t = getClass().getGenericSuperclass(); + Type[] params = ((ParameterizedType) t).getActualTypeArguments(); + return (Class) params[0]; + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/typehandler/BaseSetTypeHandler.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/typehandler/BaseSetTypeHandler.java new file mode 100644 index 0000000..084bd1e --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/typehandler/BaseSetTypeHandler.java @@ -0,0 +1,56 @@ +package com.sonic.daosupport.mysql.typehandler; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Set; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.google.common.collect.ImmutableSet; + +/** + * @author code + */ +@MappedTypes({Set.class}) +@MappedJdbcTypes(JdbcType.VARCHAR) +public abstract class BaseSetTypeHandler extends BaseTypeHandler> { + + private Class type = getGenericType(); + + @Override + public void setNonNullParameter(PreparedStatement preparedStatement, int i, + Set list, JdbcType jdbcType) throws SQLException { + preparedStatement.setString(i, JSONArray.toJSONString(list, SerializerFeature.WriteMapNullValue, + SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty)); + } + + @Override + public Set getNullableResult(ResultSet resultSet, String s) throws SQLException { + return ImmutableSet.copyOf(JSONArray.parseArray(resultSet.getString(s), type)); + } + + @Override + public Set getNullableResult(ResultSet resultSet, int i) throws SQLException { + return ImmutableSet.copyOf(JSONArray.parseArray(resultSet.getString(i), type)); + } + + @Override + public Set getNullableResult(CallableStatement callableStatement, int i) throws SQLException { + return ImmutableSet.copyOf(JSONArray.parseArray(callableStatement.getString(i), type)); + } + + private Class getGenericType() { + Type t = getClass().getGenericSuperclass(); + Type[] params = ((ParameterizedType) t).getActualTypeArguments(); + return (Class) params[0]; + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/typehandler/SetCommaTypeHandler.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/typehandler/SetCommaTypeHandler.java new file mode 100644 index 0000000..045ced1 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/mysql/typehandler/SetCommaTypeHandler.java @@ -0,0 +1,62 @@ +package com.sonic.daosupport.mysql.typehandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; + +import com.google.common.base.Splitter; +import com.google.common.collect.Sets; + +/** + * 1. 用于将数据库中的, 逗号分割的String, 转换为Set. + * 2. 存储时将Set直接存String, 多个使用逗号分割 + * @author code + */ +@MappedJdbcTypes({JdbcType.VARCHAR}) +public class SetCommaTypeHandler extends BaseTypeHandler> { + + private static final String DELIMITER = ","; + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, Set parameter, JdbcType jdbcType) throws SQLException { + String value = Sets.newLinkedHashSet(Optional.ofNullable(parameter).orElse(Collections.emptySet())) + .stream() + .collect(Collectors.joining(DELIMITER)); + + ps.setString(i, value); + } + + @Override + public Set getNullableResult(ResultSet rs, String columnName) throws SQLException { + return getSet(rs.getString(columnName)); + } + + @Override + public Set getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return getSet(rs.getString(columnIndex)); + } + + @Override + public Set getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return getSet(cs.getString(columnIndex)); + } + + private Set getSet(String dbValue) { + return Splitter.on(",") + .omitEmptyStrings() + .trimResults() + .splitToList(Optional.of(dbValue).orElse(StringUtils.EMPTY)) + .stream() + .collect(Collectors.toSet()); + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/CriteriaField.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/CriteriaField.java new file mode 100644 index 0000000..ce1d39c --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/CriteriaField.java @@ -0,0 +1,44 @@ +package com.sonic.daosupport.wrapper; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author code + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface CriteriaField { + /** + * 对应Mysql Entity的字段名或者"Field__Operator"格式 + * @return String + */ + String field() default ""; + + Operator operator() default Operator.EQ; + + /** + * 默认为true,当value为空集合的时候,自动过滤该查询条件. + * 主要是考虑到调用方在用fastjson序列化的时候,会将null集合序列化为空集合 + * 所以默认将空集合过滤掉 + * + * @return boolean + */ + boolean filterEmpty() default true; + + /** + * 默认为true,当value为null的时候,自动过滤该查询条件. + * + * @return boolean + */ + boolean filterNull() default true; + + /** + * 是否忽略该字段的查询条件 + * + * @return + */ + boolean ignore() default false; +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/CriteriaWrapper.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/CriteriaWrapper.java new file mode 100644 index 0000000..d864570 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/CriteriaWrapper.java @@ -0,0 +1,284 @@ +package com.sonic.daosupport.wrapper; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.springframework.cglib.beans.BeanMap; +import org.springframework.util.CollectionUtils; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * @author code + */ +@Slf4j +public class CriteriaWrapper { + + private static final String DELIMITER = "__"; + + /** 使用ConcurrentMap,HashMap线程不安全 */ + private static ConcurrentMap> criteriaFieldAnnotations = Maps.newConcurrentMap(); + + @Getter + private Boolean andOperator = true; + + @Getter + private ImmutableListMultimap queryFieldMap; + + public CriteriaWrapper() { + queryFieldMap = ImmutableListMultimap.of(); + } + + public CriteriaWrapper(ImmutableListMultimap queryFieldMap, boolean andOperator) { + this.queryFieldMap = queryFieldMap; + this.andOperator = andOperator; + } + + public static CriteriaWrapperBuilder builder() { + return new CriteriaWrapperBuilder(); + } + + public static class CriteriaWrapperBuilder { + private List queryFields = Lists.newArrayList(); + private boolean andOperator = true; + + /** + * @param key java bean的字段名或者"Field__Operator"格式 + * @param value 除了IS_NULL, IS_NOT_NULL允许null值外,其他情况如果为null该查询条件会被过滤 + * @return CriteriaWrapperBuilder + */ + public CriteriaWrapperBuilder queryField(String key, Object value) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(key)); + return queryField(key, QueryField.getDefaultOperator(key, value), value); + } + + /** + * + * @param key java bean的字段名或者"Field__Operator"格式 + * @param defaultOperator Operator + * @param value 除了IS_NULL, IS_NOT_NULL允许null值外,其他情况如果为null该查询条件会被过滤 + * @return CriteriaWrapperBuilder + */ + public CriteriaWrapperBuilder queryField(String key, Operator defaultOperator, Object value) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(key)); + + QueryField queryField = QueryField.buildWithNullFilter(key, defaultOperator, value); + if (queryField != null) { + this.queryFields.add(queryField); + } + + return this; + } + + /** + * + * @param condition false的时候,该查询条件会被过滤 + * @param key java bean的字段名或者"Field__Operator"格式 + * @param value 如果为null,该查询条件会被过滤 + * @return CriteriaWrapperBuilder + */ + public CriteriaWrapperBuilder queryField(boolean condition, String key, Object value) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(key)); + return queryField(condition, key, QueryField.getDefaultOperator(key, value), value); + } + + /** + * + * @param condition false的时候,该查询条件会被过滤 + * @param key java bean的字段名或者"Field__Operator"格式 + * @param defaultOperator 默认的查询Operator + * @param value 如果为null,该查询条件会被过滤 + * @return CriteriaWrapperBuilder + */ + public CriteriaWrapperBuilder queryField(boolean condition, String key, Operator defaultOperator, Object value) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(key)); + + if (!condition) { + return this; + } + + queryFields.add(QueryField.build(key, defaultOperator, value)); + return this; + } + + /** + * + * @param params {key, value}, key支持"Field__Operator"格式;value如果为null,该查询条件会被过滤 + * @return CriteriaWrapperBuilder + */ + public CriteriaWrapperBuilder queryField(Map params) { + return queryField(params, null); + } + + /** + * + * @param params {key, value}, key支持"Field__Operator"格式;value如果为null,该查询条件会被过滤 + * @param converter 对querfield进行转换. 比如查询条件, 查询值. + * @return CriteriaWrapperBuilder + */ + public CriteriaWrapperBuilder queryField(Map params, Function converter) { + List queryFields = params.entrySet().stream() + .map(entry -> QueryField.build(entry.getKey(), entry.getValue())) + .map(queryField -> converter == null ? queryField : converter.apply(queryField)) + .filter(Objects::nonNull) + .filter(queryField -> queryField.getValue() != null || queryField.getOperator().allowNullValue()) + .collect(Collectors.toList()); + this.queryFields.addAll(queryFields); + return this; + } + + public CriteriaWrapperBuilder queryFields(List queryFields) { + this.queryFields.addAll(queryFields); + return this; + } + + public CriteriaWrapperBuilder andOperator(boolean andOperator) { + this.andOperator = andOperator; + return this; + } + + public CriteriaWrapper build() { + ImmutableListMultimap multimap = queryFields.stream() + .collect(ImmutableListMultimap.toImmutableListMultimap(QueryField::getField, Function.identity())); + return new CriteriaWrapper(multimap, this.andOperator); + } + } + + /** + * 从bean对象来构造一个CriteriaWrapper + * 需要这个bean对象中查询的属性通过{@link CriteriaField}来声明 + * @param bean 查询条件的bean对象 + * @return 查询对象 + */ + public static CriteriaWrapper fromBean(Object bean) { + return fromBean(bean, true); + } + + + /** + * 从bean对象来构造一个CriteriaWrapper + * 需要这个bean对象中查询的属性通过{@link CriteriaField}来声明 + * @param bean 查询条件的bean对象 + * @param andOperator true查询条件是and操作, false查询条件or操作. + * @return 查询对象 + */ + public static CriteriaWrapper fromBean(Object bean, boolean andOperator) { + return fromBean(bean, andOperator, null); + } + + /** + * 从bean对象来构造一个CriteriaWrapper + * 需要这个bean对象中查询的属性通过{@link CriteriaField}来声明 + * @param bean 查询条件的bean对象 + * @param andOperator true查询条件是and操作, false查询条件or操作. + * @return 查询对象 + */ + public static CriteriaWrapper fromBean(Object bean, boolean andOperator, + Function> converter) { + Map annotations = getFieldAnnotations(bean.getClass()); + + Map beanMap = BeanMap.create(bean); + List queryFields = beanMap.entrySet().stream() + .map(entry -> QueryField.build(entry.getKey(), entry.getValue(), annotations)) + .filter(Objects::nonNull) + .flatMap(queryField -> converter == null ? Stream.of(queryField) : converter.apply(queryField).stream()) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + return CriteriaWrapper.builder().queryFields(queryFields).andOperator(andOperator).build(); + } + + + private static Map getFieldAnnotations(Class beanClazz) { + return criteriaFieldAnnotations.computeIfAbsent(beanClazz, clazz -> + FieldUtils.getFieldsListWithAnnotation(clazz, CriteriaField.class).stream() + .collect(Collectors.toMap(Field::getName, field -> field.getAnnotation(CriteriaField.class)))); + } + + @Builder(toBuilder = true) + @Data + @AllArgsConstructor + public static class QueryField { + /** java bean的property name */ + private String field; + /** 查询的operator */ + private Operator operator; + /** 查询的值 */ + private Object value; + + private static QueryField build(String field, Object value, Map annotations) { + CriteriaField fieldAnnotation = annotations.get(field); + if (fieldAnnotation == null) { + // 如果没有Annotation,默认会过滤Null + return QueryField.buildWithNullFilter(field, getDefaultOperator(field, value), value); + } + + if (fieldAnnotation.ignore()) { + return null; + } + + if (fieldAnnotation.filterEmpty() + && (value instanceof Collection) + && CollectionUtils.isEmpty((Collection)value)) { + return null; + } + + if (fieldAnnotation.filterNull() && Objects.isNull(value)) { + return null; + } + + String fieldName = Strings.isNullOrEmpty(fieldAnnotation.field()) ? field : fieldAnnotation.field(); + // XXX 注解中获取的operator如果为EQ,无法判断是用户手动设置为EQ还是使用的默认operator.EQ,这里暂时保持原状 + return QueryField.build(fieldName, fieldAnnotation.operator(), value); + } + + private static QueryField build(String key, Object value) { + return QueryField.build(key, getDefaultOperator(key, value), value); + } + + private static QueryField build(String key, Operator defaultOperator, Object value) { + String field = StringUtils.substringBefore(key, DELIMITER); + String expression = StringUtils.substringAfter(key, DELIMITER); + Operator operator = Strings.isNullOrEmpty(expression) ? defaultOperator : Operator.valueOf(expression); + return new QueryField(field, operator, value); + } + + private static QueryField buildWithNullFilter(String key, Operator defaultOperator, Object value) { + QueryField queryField = build(key, defaultOperator, value); + if (queryField.getValue() == null && !queryField.getOperator().allowNullValue()) { + return null; + } + return queryField; + } + + private static Operator getDefaultOperator(String field, Object value) { + if (!(value instanceof Collection)) { + return Operator.EQ; + } + + // 当value instanceof Collection,检查值是否为空 + if (((Collection)value).isEmpty()) { + log.error("value is collection and is empty, field={}", field); + } + return Operator.IN; + } + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/Operator.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/Operator.java new file mode 100644 index 0000000..cafc7c5 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/Operator.java @@ -0,0 +1,31 @@ +package com.sonic.daosupport.wrapper; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author code + */ +@Getter +@AllArgsConstructor +public enum Operator { + /** 数据库常规操作符 */ + IN, + LIKE, + EQ, + NE, + GT, + GE, + LT, + LE, + IS_NULL, + IS_NOT_NULL, + BETWEEN, + ORDER, + @Deprecated + CUSTOM; + + public boolean allowNullValue() { + return this == IS_NULL || this == IS_NOT_NULL; + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/OperatorProcessor.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/OperatorProcessor.java new file mode 100644 index 0000000..acd8b31 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/OperatorProcessor.java @@ -0,0 +1,30 @@ +package com.sonic.daosupport.wrapper; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.google.common.collect.ImmutableListMultimap; + +/** + * 封装底层的查询实现 + * @author code + */ +public interface OperatorProcessor { + + /** + * 组装所有查询条件 + * @param queryColumnMap + * @param andOperator + * @return + */ + QueryWrapper assembleAllQueryWrapper(ImmutableListMultimap queryColumnMap, boolean andOperator); + + /** + * TODO: 暂时不支持返回 LambdaQueryWrapper + * 组装成 lambda 查询条件对象 + * @param queryColumnMap + * @param andOperator + * @return + */ + LambdaQueryWrapper assembleAllLambdaQueryWrapper(ImmutableListMultimap queryColumnMap, boolean andOperator); + +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/ReflectionUtils.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/ReflectionUtils.java new file mode 100644 index 0000000..1c77d04 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/ReflectionUtils.java @@ -0,0 +1,113 @@ +package com.sonic.daosupport.wrapper; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import org.springframework.util.CollectionUtils; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +/** + * copy from mybatis, com.baomidou.mybatisplus.core.toolkit.ReflectionKit + * @author code + */ +public class ReflectionUtils { + + /** + * class field cache + */ + private static final Map, List> CLASS_FIELD_CACHE = new ConcurrentHashMap<>(); + + private static final Map, Class> PRIMITIVE_WRAPPER_TYPE_MAP = new IdentityHashMap<>(8); + + static { + PRIMITIVE_WRAPPER_TYPE_MAP.put(Boolean.class, boolean.class); + PRIMITIVE_WRAPPER_TYPE_MAP.put(Byte.class, byte.class); + PRIMITIVE_WRAPPER_TYPE_MAP.put(Character.class, char.class); + PRIMITIVE_WRAPPER_TYPE_MAP.put(Double.class, double.class); + PRIMITIVE_WRAPPER_TYPE_MAP.put(Float.class, float.class); + PRIMITIVE_WRAPPER_TYPE_MAP.put(Integer.class, int.class); + PRIMITIVE_WRAPPER_TYPE_MAP.put(Long.class, long.class); + PRIMITIVE_WRAPPER_TYPE_MAP.put(Short.class, short.class); + } + + /** + *

+ * 获取该类的所有属性列表 + *

+ * + * @param clazz 反射类 + */ + public static List getFieldList(Class clazz) { + if (Objects.isNull(clazz)) { + return Collections.emptyList(); + } + List fields = CLASS_FIELD_CACHE.get(clazz); + if (CollectionUtils.isEmpty(fields)) { + synchronized (CLASS_FIELD_CACHE) { + fields = doGetFieldList(clazz); + CLASS_FIELD_CACHE.put(clazz, fields); + } + } + return fields; + } + + /** + *

+ * 获取该类的所有属性列表 + *

+ * + * @param clazz 反射类 + */ + public static List doGetFieldList(Class clazz) { + if (clazz.getSuperclass() != null) { + /* 排除重载属性 */ + Map fieldMap = excludeOverrideSuperField(clazz.getDeclaredFields(), + /* 处理父类字段 */ + getFieldList(clazz.getSuperclass())); + List fieldList = new ArrayList<>(); + /* + * 重写父类属性过滤后处理忽略部分,支持过滤父类属性功能 + * 场景:中间表不需要记录创建时间,忽略父类 createTime 公共属性 + * 中间表实体重写父类属性 ` private transient Date createTime; ` + */ + fieldMap.forEach((k, v) -> { + /* 过滤静态属性 */ + if (!Modifier.isStatic(v.getModifiers()) + /* 过滤 transient关键字修饰的属性 */ + && !Modifier.isTransient(v.getModifiers())) { + fieldList.add(v); + } + }); + return fieldList; + } else { + return Collections.emptyList(); + } + } + + /** + *

+ * 排序重置父类属性 + *

+ * + * @param fields 子类属性 + * @param superFieldList 父类属性 + */ + public static Map excludeOverrideSuperField(Field[] fields, List superFieldList) { + // 子类属性 + Map fieldMap = Stream.of(fields).collect(toMap(Field::getName, identity())); + superFieldList.stream().filter(field -> !fieldMap.containsKey(field.getName())) + .forEach(f -> fieldMap.put(f.getName(), f)); + return fieldMap; + } + +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/SimpleWrapperConverter.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/SimpleWrapperConverter.java new file mode 100644 index 0000000..3dc77aa --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/SimpleWrapperConverter.java @@ -0,0 +1,78 @@ +package com.sonic.daosupport.wrapper; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.GlobalResultCode; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; + +/** + * 将CriteriaWrapper转换为mysql的QueryWrapper + * @author code + */ +@Slf4j +@Builder +@AllArgsConstructor +public class SimpleWrapperConverter { + + private final OperatorProcessor operatorProcessor; + private final Map fieldColumnMap; + + public QueryWrapper toWrapper(CriteriaWrapper criteriaWrapper) { + ListMultimap multimap = criteriaWrapper.getQueryFieldMap() + .asMap().entrySet().stream() + .collect(Multimaps.flatteningToMultimap( + query -> getColumnNotNull(query.getKey()), + query -> convertValue(query.getValue()).stream(), + ArrayListMultimap::create)); + return operatorProcessor.assembleAllQueryWrapper(ImmutableListMultimap.copyOf(multimap), criteriaWrapper.getAndOperator()); + } + + /** + * TODO: 暂时不支持返回 LambdaQueryWrapper + * @param criteriaWrapper + * @return + */ + public LambdaQueryWrapper toLambdaWrapper(CriteriaWrapper criteriaWrapper) { + ListMultimap multimap = criteriaWrapper.getQueryFieldMap() + .asMap().entrySet().stream() + .collect(Multimaps.flatteningToMultimap( + query -> getColumnNotNull(query.getKey()), + query -> convertValue(query.getValue()).stream(), + ArrayListMultimap::create)); + return operatorProcessor.assembleAllLambdaQueryWrapper(ImmutableListMultimap.copyOf(multimap), criteriaWrapper.getAndOperator()); + } + + public Optional getColumn(String fieldName) { + return Optional.ofNullable(fieldColumnMap.get(fieldName)); + } + + public String getColumnNotNull(String fieldName) { + return getColumn(fieldName).orElseThrow(() -> new BizException(GlobalResultCode.INVALID_PARAMS)); + } + + /** + * 转换QueryField的value + * + * @param queryFields + * @return + */ + private List convertValue(Collection queryFields) { + return queryFields.stream() + .map(queryField -> queryField.toBuilder().value(queryField.getValue()).build()) + .collect(Collectors.toList()); + } +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/StringUtils.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/StringUtils.java new file mode 100644 index 0000000..f303a63 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/StringUtils.java @@ -0,0 +1,134 @@ +package com.sonic.daosupport.wrapper; + +/** + * XXX: copy and trim from common-lang3 -> StringUtils.{@link org.apache.commons.lang3.StringUtils} + * to avoid dependency to common-lang3. + */ +class StringUtils { + + /** + * The empty String {@code ""}. + * + * @since 2.0 + */ + public static final String EMPTY = ""; + /** + * Represents a failed index search. + * + * @since 2.1 + */ + public static final int INDEX_NOT_FOUND = -1; + + // SubStringAfter/SubStringBefore + //----------------------------------------------------------------------- + + /** + *

Gets the substring before the first occurrence of a separator. + * The separator is not returned.

+ * + *

A {@code null} string input will return {@code null}. + * An empty ("") string input will return the empty string. + * A {@code null} separator will return the input string.

+ * + *

If nothing is found, the string input is returned.

+ * + *
+     * StringUtils.substringBefore(null, *)      = null
+     * StringUtils.substringBefore("", *)        = ""
+     * StringUtils.substringBefore("abc", "a")   = ""
+     * StringUtils.substringBefore("abcba", "b") = "a"
+     * StringUtils.substringBefore("abc", "c")   = "ab"
+     * StringUtils.substringBefore("abc", "d")   = "abc"
+     * StringUtils.substringBefore("abc", "")    = ""
+     * StringUtils.substringBefore("abc", null)  = "abc"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring before the first occurrence of the separator, + * {@code null} if null String input + * @since 2.0 + */ + public static String substringBefore(final String str, final String separator) { + if (isEmpty(str) || separator == null) { + return str; + } + if (separator.isEmpty()) { + return EMPTY; + } + final int pos = str.indexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return str; + } + return str.substring(0, pos); + } + + /** + *

Gets the substring after the first occurrence of a separator. + * The separator is not returned.

+ * + *

A {@code null} string input will return {@code null}. + * An empty ("") string input will return the empty string. + * A {@code null} separator will return the empty string if the + * input string is not {@code null}.

+ * + *

If nothing is found, the empty string is returned.

+ * + *
+     * StringUtils.substringAfter(null, *)      = null
+     * StringUtils.substringAfter("", *)        = ""
+     * StringUtils.substringAfter(*, null)      = ""
+     * StringUtils.substringAfter("abc", "a")   = "bc"
+     * StringUtils.substringAfter("abcba", "b") = "cba"
+     * StringUtils.substringAfter("abc", "c")   = ""
+     * StringUtils.substringAfter("abc", "d")   = ""
+     * StringUtils.substringAfter("abc", "")    = "abc"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring after the first occurrence of the separator, + * {@code null} if null String input + * @since 2.0 + */ + public static String substringAfter(final String str, final String separator) { + if (isEmpty(str)) { + return str; + } + if (separator == null) { + return EMPTY; + } + final int pos = str.indexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return EMPTY; + } + return str.substring(pos + separator.length()); + } + + // Empty checks + //----------------------------------------------------------------------- + + /** + *

Checks if a CharSequence is empty ("") or null.

+ * + *
+     * StringUtils.isEmpty(null)      = true
+     * StringUtils.isEmpty("")        = true
+     * StringUtils.isEmpty(" ")       = false
+     * StringUtils.isEmpty("bob")     = false
+     * StringUtils.isEmpty("  bob  ") = false
+     * 
+ * + *

NOTE: This method changed in Lang version 2.0. + * It no longer trims the CharSequence. + * That functionality is available in isBlank().

+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is empty or null + * @since 3.0 Changed signature from isEmpty(String) to isEmpty(CharSequence) + */ + public static boolean isEmpty(final CharSequence cs) { + return cs == null || cs.length() == 0; + } + +} diff --git a/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/TriConsumer.java b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/TriConsumer.java new file mode 100644 index 0000000..43d6a03 --- /dev/null +++ b/sonic-common/common-lib-box/dao-support-lib/src/main/java/com/sonic/daosupport/wrapper/TriConsumer.java @@ -0,0 +1,18 @@ +package com.sonic.daosupport.wrapper; + +import com.baomidou.mybatisplus.core.conditions.AbstractWrapper; + +/** + * 仅包内可访问 + * @author code + */ +@FunctionalInterface +public interface TriConsumer { + /** + * 三入参的consumer + * @param k + * @param v + * @param s + */ + void accept(AbstractWrapper k, Object v, Object s); +} diff --git a/sonic-common/common-lib-box/pom.xml b/sonic-common/common-lib-box/pom.xml new file mode 100644 index 0000000..5580033 --- /dev/null +++ b/sonic-common/common-lib-box/pom.xml @@ -0,0 +1,59 @@ + + + + com.sonic + common-parent-pom + 1.0 + + 4.0.0 + + common-lib-box + pom + 1.0 + basic services common library + + + common-lib + dao-support-lib + + + + + org.projectlombok + lombok + 1.18.40 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + ${java.version} + ${java.version} + utf-8 + + + + + org.apache.maven.plugins + maven-source-plugin + 3.1.0 + + + attach-sources + + jar + + + + + + + + diff --git a/sonic-common/common-parent-pom/pom.xml b/sonic-common/common-parent-pom/pom.xml new file mode 100644 index 0000000..dfafd7d --- /dev/null +++ b/sonic-common/common-parent-pom/pom.xml @@ -0,0 +1,283 @@ + + + + org.springframework.boot + spring-boot-starter-parent + 2.2.1.RELEASE + + 4.0.0 + + com.sonic + common-parent-pom + 1.0 + basic services common parent pom + + pom + + + 1.8 + + 1.18.40 + 28.0-jre + 1.2.70 + 1.0.3 + 3.8.1 + 1.2.1 + 1.4 + 1.8.3 + 1.6 + 4.2.8 + + 2.8.0 + 28.0-jre + 2.3.3 + 2.10.0 + + 4.3.1 + 2.6.0 + 2.6.0 + + + 3.2.0 + 5.1.47 + 1.1.21 + + + 2.7.0 + + + 1.4.199 + 0.1.14 + 0.7.2 + 5.3.2 + + + + + + org.springframework.boot + spring-boot-starter + + + + org.hashids + hashids + ${hashids.version} + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + com.alibaba + fastjson + ${fastjson.version} + + + com.googlecode.aviator + aviator + ${aviator.version} + + + + com.google.guava + guava + ${guava.version} + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + + com.google.errorprone + error_prone_annotations + ${error_prone_annotations.version} + compile + + + org.checkerframework + checker-qual + ${checker-qual.version} + + + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + com.squareup.retrofit2 + retrofit + ${retrofit2.version} + + + com.squareup.retrofit2 + converter-jackson + ${retrofit2-converter-jackson.version} + + + + + mysql + mysql-connector-java + ${mysql-connector.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + + com.alibaba + druid-spring-boot-starter + ${druid.starter.version} + + + + com.auth0 + java-jwt + ${jwt.version} + + + + + org.springframework.amqp + spring-amqp + 2.4.1 + + + org.springframework.amqp + spring-rabbit + 2.4.1 + + + + commons-fileupload + commons-fileupload + ${commons-fileupload.version} + + + commons-io + commons-io + ${commons-io.version} + + + commons-beanutils + commons-beanutils + ${commons-beanutils.version} + + + org.apache.commons + commons-text + ${commons-text.version} + + + + + io.springfox + springfox-swagger2 + ${swagger2.version} + + + * + org.springframework + + + guava + com.google.guava + + + + + io.springfox + springfox-swagger-ui + ${swagger2.version} + + + + + com.h2database + h2 + ${h2.version} + test + + + com.github.fppt + jedis-mock + ${redis-mock.version} + test + + + it.ozimov + embedded-redis + ${embedded-redis.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + + + + + + + pl.project13.maven + git-commit-id-plugin + 2.2.6 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + ${java.version} + ${java.version} + utf-8 + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 1.4.1 + + + enforce + + + + + + + enforce + + + + + + + + diff --git a/sonic-cow/.gitignore b/sonic-cow/.gitignore new file mode 100644 index 0000000..51bb6a0 --- /dev/null +++ b/sonic-cow/.gitignore @@ -0,0 +1,26 @@ +/**/rebel.xml + +#target/ +target/ + +# IDEA # +.idea/ +*.iml + +# Eclipse # +.settings/ +.classpath +.project + +# Log # +logs/ +*.log% + +#mac +.DS_Store + +#svn +.svn +#reviewboardrc +.reviewboardrc +**/rest-client.private.env.json diff --git a/sonic-cow/bootstrap-guide.md b/sonic-cow/bootstrap-guide.md new file mode 100644 index 0000000..ba12dc6 --- /dev/null +++ b/sonic-cow/bootstrap-guide.md @@ -0,0 +1,41 @@ +# 项目定制化手册 +## 定制化步骤 +* 确定项目依赖的组件比如redis,mysql,rabbitmq, 然后搜索`TODO`把不需要的依赖和多余的目录移除. +* 确定项目依赖组件后, 请在application-${env}中配置对应的资源地址. +* 运行单元测试, 保证单元测试全部通过. + +## 模块介绍 +### common +在该模块添加其他模块共用的lib,例如common-lib以及常用的guava,fastjson等
+主要是考虑到项目可能有多个部署的模块,通过将共用的lib定义在common模块中,可以简化其他模块的配置 + +### server +可部署的后端服务,包含SpringBoot的入口以及该服务相关的client,config,entity,dao, service,controller等 + +#### config +定义配置信息和错误code + +#### client +定义访问依赖的第三方服务的客户端接口. 访问依赖方服务,必须通过Client接口封装,禁止业务代码调用http相关逻辑. + +#### entity +定义领域对象. + +#### service +主要定义业务逻辑代码 + +#### controller +对外暴露的API定义 + +#### test +单元测试模块. 为了保证交付的质量和服务的演进,核心逻辑需要编写单元测试, + +##### 目录文件 +- java + - ClientStubs 第三方依赖客户端的Stub实现. + - BaseTest 单元测试基类. 建议每个单元测试从它基础 +- resources + - mysql 存放数据库的schema和测试数据. schema文件可以作为schema变化的版本记录, 同时也是H2数据库初始化脚本. + +### integration-test +集成测试,测试已部署服务的APIs diff --git a/sonic-cow/common/pom.xml b/sonic-cow/common/pom.xml new file mode 100644 index 0000000..be4901b --- /dev/null +++ b/sonic-cow/common/pom.xml @@ -0,0 +1,58 @@ + + + + sonic-cow + com.sonic.bs + 1.0-SNAPSHOT + + 4.0.0 + + sonic-cow-common + jar + 1.0-SNAPSHOT + + + + com.sonic + common-lib + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + mysql + mysql-connector-java + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.google.guava + guava + + + com.alibaba + fastjson + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-pool2 + + + org.springframework.boot + spring-boot-starter-data-redis + + + diff --git a/sonic-cow/common/src/main/java/com/sonic/cow/common/GlobalConfig.java b/sonic-cow/common/src/main/java/com/sonic/cow/common/GlobalConfig.java new file mode 100644 index 0000000..32e7040 --- /dev/null +++ b/sonic-cow/common/src/main/java/com/sonic/cow/common/GlobalConfig.java @@ -0,0 +1,122 @@ +package com.sonic.cow.common; + +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.AppRuntime; +import com.sonic.common.client.JobmanClient; +import com.sonic.common.client.impl.JobmanClientImpl; +import com.sonic.common.log.RequestLogAspect; +import com.sonic.common.rpc.HttpClient; +import com.sonic.common.rpc.OkHttpClientImpl; +import com.sonic.common.rpc.RpcClient; +import com.sonic.common.rpc.RpcClientImpl; +import com.sonic.common.stat.FrequencyAlertInterceptor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.task.TaskExecutorBuilder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.task.TaskExecutor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.stream.Collectors; + +import static org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME; + +/** + * @author chenjun + */ +@Slf4j +public class GlobalConfig { + @Value("${spring.application.name}") + private String appName; + @Value("${spring.application.id}") + private String appId; + + @Bean + ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() { + return new ScheduledThreadPoolExecutor(1); + } + + /** + * XXX Spring默认配置当有Executor的bean后不再装载TaskExecutor, 这里因为手动注册了 + * {@link #scheduledThreadPoolExecutor}会导致spring不再自动注册TaskExecutor, 因此需要去掉ConditionalOnMissingBean的限制. + * 参考{@link TaskExecutionAutoConfiguration#applicationTaskExecutor} + */ + @Bean(name = {APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME}) + public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { + return builder.build(); + } + + @Bean + public RequestLogAspect requestLogAspect() { + return new RequestLogAspect(); + } + + @Bean + AppRuntime appRuntime(ApplicationContext applicationContext) { + return AppRuntime.builder().appId(appId).appName(appName).applicationContext(applicationContext).build(); + } + + @Bean + public HttpClient httpClient() { + return new OkHttpClientImpl(); + } + + + @Bean + public JobmanClient jobmanClient(AppRuntime appRuntime, TaskExecutor taskExecutor, RedisTemplate redisTemplate) { + return new JobmanClientImpl.Builder().appRuntime(appRuntime).taskExecutor(taskExecutor).redisTemplate(redisTemplate).build(); + } + + /** + * 用于配置接口访问频率超限告警, 接口频率配置参考application.yml中的web.frequency-alert.rules + * TODO 根据需要配置为输出到日志或者alertClient + * + * @return + */ + @Bean + public FrequencyAlertInterceptor frequencyAlertInterceptor(RuleConfig ruleConfig) { + return new FrequencyAlertInterceptor(ruleConfig.getAlertRuleMap(), + (request, frequencyAlert) -> log.warn("frequency alert, api frequency alert, url = {} alert = {}", + request.getRequestURI(), JSONObject.toJSONString(frequencyAlert))); + } + + @Bean + @Primary + public RpcClient rpcClient() { + return new RpcClientImpl(HttpClient.Config.EMPTY); + } + + @Data + @Component + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "web.frequency-alert") + public static class RuleConfig { + private List rules; + + public Map getAlertRuleMap() { + if (rules == null) { + return Collections.emptyMap(); + } + return rules.stream() + .map(e -> { + String[] split = e.split(":"); + return new AbstractMap.SimpleEntry<>(split[0], split[1]); + }).collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); + } + } +} diff --git a/sonic-cow/common/src/main/java/com/sonic/cow/common/MybatisPlusConfig.java b/sonic-cow/common/src/main/java/com/sonic/cow/common/MybatisPlusConfig.java new file mode 100644 index 0000000..c3d65c8 --- /dev/null +++ b/sonic-cow/common/src/main/java/com/sonic/cow/common/MybatisPlusConfig.java @@ -0,0 +1,19 @@ +package com.sonic.cow.common; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; + +@EnableTransactionManagement +@MapperScan("com.sonic.**.dao") +public class MybatisPlusConfig { + + @Bean + public PaginationInterceptor paginationInterceptor() { + PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); + paginationInterceptor.setDialectType("mysql"); + return paginationInterceptor; + } +} diff --git a/sonic-cow/lib/pom.xml b/sonic-cow/lib/pom.xml new file mode 100644 index 0000000..66c661e --- /dev/null +++ b/sonic-cow/lib/pom.xml @@ -0,0 +1,55 @@ + + + + com.sonic.bs + sonic-cow + 1.0-SNAPSHOT + + 4.0.0 + + com.sonic.cow + sonic-cow-lib + jar + 1.0-SNAPSHOT + + + + + com.sonic + common-lib + + + + log4j-api + org.apache.logging.log4j + + + com.alibaba + fastjson + + + + + + com.alibaba + fastjson + 1.2.83 + + + + + + + + releases + http://121.196.56.236:8081/repository/maven-releases/ + + + snapshots + http://121.196.56.236:8081/repository/maven-snapshots/ + + + + \ No newline at end of file diff --git a/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/ContentClient.java b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/ContentClient.java new file mode 100644 index 0000000..119101f --- /dev/null +++ b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/ContentClient.java @@ -0,0 +1,73 @@ +package com.sonic.cow.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.Maps; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.cow.lib.input.GenAiUserContentV1Input; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/** + * @author code + */ +@Slf4j +@Service +public class ContentClient { + + private static final String GEN_SUP_CONTENT_URL = "/api/gen/sup-content"; + private static final String GEN_USER_CONTENT_URL = "/api/gen/user-content"; + + + private RpcClient rpcClient; + private String host; + + public ContentClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://dev-cow.svc.cloud"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-cow-svc:8080"; + break; + case product: + default: + this.host = "http://prod-cow-svc:8080"; + } + } + + /** + * 生成辅助聊天内容 + * + * @param userId + * @param aiId + * @return + */ + public List genSupContent(Long userId, Long aiId) { + Map params = Maps.newHashMap(); + params.put("userId", userId); + params.put("aiId", aiId); + return rpcClient.postBodySign(host + GEN_SUP_CONTENT_URL, params, new TypeReference>>() { + }); + } + + /** + * 一键生成 + * @param input + * @return + */ + public String genUserContent(GenAiUserContentV1Input input) { + return rpcClient.postBodySign(host + GEN_USER_CONTENT_URL, input, new TypeReference>() { + }); + } + + +} diff --git a/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/ImageClient.java b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/ImageClient.java new file mode 100644 index 0000000..1b31c99 --- /dev/null +++ b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/ImageClient.java @@ -0,0 +1,59 @@ +package com.sonic.cow.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.Maps; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/** + * @author code + */ +@Slf4j +@Service +public class ImageClient { + + private static final String CHECK_IMAGE_IS_AI_GENERATED = "/api/checkImageIsAIGenerated"; + + private RpcClient rpcClient; + private String host; + + public ImageClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://dev-cow.svc.cloud"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-cow-svc:8080"; + break; + case product: + default: + this.host = "http://prod-cow-svc:8080"; + } + } + + /** + * 添加相册图片,背景图片,ai形象图时,检查图片是否是AI生成的 + * + * @param userId + * @param imgUrlList + * @return + */ + public Void checkImageIsAIGenerated(Long userId, List imgUrlList) { + Map params = Maps.newHashMap(); + params.put("userId", userId); + params.put("imgUrlList", imgUrlList); + return rpcClient.postBodySign(host + CHECK_IMAGE_IS_AI_GENERATED, params, new TypeReference>() { + }); + } + +} diff --git a/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/NsfwCheckClient.java b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/NsfwCheckClient.java new file mode 100644 index 0000000..7c9540b --- /dev/null +++ b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/NsfwCheckClient.java @@ -0,0 +1,52 @@ +package com.sonic.cow.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.cow.lib.input.NsfwCheckInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author code + */ +@Slf4j +@Service +public class NsfwCheckClient { + + private static final String NSFW_CHECK = "/api/nsfw/check"; + + private RpcClient rpcClient; + private String host; + + public NsfwCheckClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://dev-cow.svc.cloud"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-cow-svc:8080"; + break; + case product: + default: + this.host = "http://prod-cow-svc:8080"; + } + } + + /** + * 敏感词校验 + * @param content + */ + public void checkContent(String content) { + NsfwCheckInput input = NsfwCheckInput.builder() + .content(content) + .build(); + rpcClient.postBodySign(host + NSFW_CHECK, input, new TypeReference>() {}); + } + +} diff --git a/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/VoiceClient.java b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/VoiceClient.java new file mode 100644 index 0000000..911ff97 --- /dev/null +++ b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/client/VoiceClient.java @@ -0,0 +1,56 @@ +package com.sonic.cow.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.Maps; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.cow.lib.input.VoiceTtsInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/** + * @author code + */ +@Slf4j +@Service +public class VoiceClient { + + private static final String TTS_URL = "/api/tts"; + + private RpcClient rpcClient; + private String host; + + public VoiceClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://dev-cow.svc.cloud"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-cow-svc:8080"; + break; + case product: + default: + this.host = "http://prod-cow-svc:8080"; + } + } + + /** + * 文本转语音 + * + * @param voiceTtsInput + * @return + */ + public String tts(VoiceTtsInput voiceTtsInput) { + return rpcClient.postBodySign(host + TTS_URL, voiceTtsInput, new TypeReference>() { + }); + } + +} diff --git a/sonic-cow/lib/src/main/java/com/sonic/cow/lib/enums/PromptTypeEnum.java b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/enums/PromptTypeEnum.java new file mode 100644 index 0000000..728364f --- /dev/null +++ b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/enums/PromptTypeEnum.java @@ -0,0 +1,72 @@ +package com.sonic.cow.lib.enums; + +public enum PromptTypeEnum { + //AI一键生成人物基础信息 AI自行创作 + GEN_PROFILE_BY_NON, + + //AI一键生成人物基础信息 AI根据用户输入进行创作 + GEN_PROFILE_BY_CONTENT, + + //AI一键生成对话风格 AI自行创作 + GEN_DIALOG_STYLE_BY_NON, + + //AI一键生成对话风格 AI根据用户输入进行创作 + GEN_DIALOG_STYLE_BY_CONTENT, + + //AI一键生成开场白 AI自行创作 + GEN_PROLOGUE_BY_NON, + + //AI一键生成开场白 AI根据用户输入进行创作 + GEN_PROLOGUE_BY_CONTENT, + + //AI一键生成人物简介 AI总结 + GEN_INTRODUCTION, + + //形象描述 AI自行创作 + GEN_AI_IMAGE_DESC_BY_NON, + + //形象描述 AI根据用户输入进行创作 + GEN_AI_IMAGE_DESC_BY_CONTENT, + + //文生图-生成6组不同的prompt + TEXT_TO_IMAGE_PROMPT, + + //图生文-参考图生成prompt + IMAGE_REFERENCE, + + //文生图-生成6组不同的prompt + TXT_TO_IMAGE_PROMPT, + + //聊天时用的系统提示词 + CHAT_SYSTEM_PROMPT_TEMPLATE, + + //超过24小时用户未发送聊天消息时,AI主动给用户发送消息的系统提示词 + AUTO_SEND_CHAT_SYSTEM_PROMPT_TEMPLATE, + + //用户停留3分钟未发送聊天消息时,AI主动给用户发送消息的系统提示词 + AUTO_SEND_CHAT_3_MINUTES_SYSTEM_PROMPT_TEMPLATE, + + //生成9条辅助聊天内容的系统提示词 + GEN_SUP_CONTENT_SYSTEM_PROMPT_TEMPLATE, + + //语音通话时用的系统提示词 + VOICE_CHAT_SYSTEM_PROMPT_TEMPLATE, + + //聊天对话打情绪分值 + CHAT_SCORE_SYSTEM_PROMPT_TEMPLATE, + + //对话场景 + DIALOGUE_SCENARIO, + + //编辑AI或相册图片生成时,合并新老形象描述 + MERGE_NEW_OLD_IMAGE_DESC, + //获取图片的定义 + GET_AI_IMAGE_FUNCTION_CALL, + //开启语音通话时根据上下文生成的对话开场白 + START_VOICE_CHAT_DIALOGUE_PROLOGUE, + + //提取JSON内容 + EXTRACT_JSON_CONTENT, + ; + +} diff --git a/sonic-cow/lib/src/main/java/com/sonic/cow/lib/input/GenAiUserContentV1Input.java b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/input/GenAiUserContentV1Input.java new file mode 100644 index 0000000..0d543fb --- /dev/null +++ b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/input/GenAiUserContentV1Input.java @@ -0,0 +1,56 @@ +package com.sonic.cow.lib.input; + +import com.sonic.cow.lib.enums.PromptTypeEnum; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * 生成AI人物的基础信息入参 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class GenAiUserContentV1Input { + + @ApiParam(value = "昵称") + private String nickname; + + @ApiParam(value = "性别(英文)") + private String sex; + + @ApiParam(value = "生日(yyyy-MM-dd)") + private String birthday; + + @ApiParam(value = "年龄") + private Integer age; + + @ApiParam(value = "性格(字典代码)") + private String characterCode; + + @ApiParam(value = "标签(字典代码)") + private String tagCode; + + @ApiModelProperty("用户输入的内容") + private String content; + + @ApiModelProperty("ai的人物基础信息(背景、性格、身份)【生成个人简介时使用】") + private String figure; + + @ApiModelProperty("ai对话风格(角色的聊天方式、对话语气)【生成个人简介时使用】") + private String dialogue; + + @NotNull + @ApiModelProperty("提示词类型") + private PromptTypeEnum ptType; + + @ApiModelProperty("简介") + private String introduction; + +} diff --git a/sonic-cow/lib/src/main/java/com/sonic/cow/lib/input/NsfwCheckInput.java b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/input/NsfwCheckInput.java new file mode 100644 index 0000000..07d719d --- /dev/null +++ b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/input/NsfwCheckInput.java @@ -0,0 +1,23 @@ +package com.sonic.cow.lib.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * @description: + * @author: mzc + * @date: 2025-09-18 11:03 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class NsfwCheckInput { + + @ApiModelProperty("文本内容") + private String content; +} diff --git a/sonic-cow/lib/src/main/java/com/sonic/cow/lib/input/VoiceTtsInput.java b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/input/VoiceTtsInput.java new file mode 100644 index 0000000..a529d44 --- /dev/null +++ b/sonic-cow/lib/src/main/java/com/sonic/cow/lib/input/VoiceTtsInput.java @@ -0,0 +1,32 @@ +package com.sonic.cow.lib.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class VoiceTtsInput { + + @ApiModelProperty("用户id 内部使用") + private Long userId; + + @ApiModelProperty("AI id") + private Long aiId; + + @ApiModelProperty("文本内容") + private String text; + + @ApiModelProperty("语音类型【传入审核并开通的语音ID】以S_开头的ID") + private String voiceType; + + @ApiModelProperty("语速,范围 [-50,100],100代表2.0倍速,-50代表0.5倍速") + private Integer speechRate = 0; + + @ApiModelProperty("音量(Volume)。值范围为 [-12, 12]。默认:0") + private Integer pitchRate = 0; +} diff --git a/sonic-cow/pom.xml b/sonic-cow/pom.xml new file mode 100644 index 0000000..857dfe4 --- /dev/null +++ b/sonic-cow/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + com.sonic + common-parent-pom + 1.0 + + + sonic-cow + + com.sonic.bs + pom + 1.0-SNAPSHOT + + + + 1.0.6 + 1.0 + + + + + + com.sonic + common-lib + ${common-lib.version} + + + + com.sonic + dao-support-lib + ${dao-support-lib.version} + + + + + + + org.projectlombok + lombok + + + + + common + server + lib + + diff --git a/sonic-cow/server/pom.xml b/sonic-cow/server/pom.xml new file mode 100644 index 0000000..afaaa1c --- /dev/null +++ b/sonic-cow/server/pom.xml @@ -0,0 +1,179 @@ + + + + sonic-cow + com.sonic.bs + 1.0-SNAPSHOT + + 4.0.0 + + sonic-cow-server + jar + + + + com.sonic.bs + sonic-cow-common + 1.0-SNAPSHOT + + + + com.sonic.sdk + sonic-common-api + 1.0.1-SNAPSHOT + + + + com.sonic + dao-support-lib + 1.0 + + + + com.sonic.pigeon + sonic-pigeon-lib + 1.1-SNAPSHOT + + + + com.sonic.frog + sonic-frog-lib + 1.2-SNAPSHOT + + + + com.sonic.bear + sonic-bear-lib + 1.1-SNAPSHOT + + + + com.sonic.lion + sonic-lion-lib + 1.0-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + it.ozimov + embedded-redis + + + + + org.redisson + redisson-spring-boot-starter + 3.13.1 + + + + + org.springframework.amqp + spring-amqp + + + org.springframework.amqp + spring-rabbit + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + + + com.h2database + h2 + + + + + + + + + + + com.byteplus + byteplus-java-sdk-v2-ark-runtime + 0.1.21 + + + + com.sonic.shark + sonic-shark-lib + 1.0-SNAPSHOT + + + + + com.volcengine + volc-sdk-java + + 1.0.232 + + + jsr305 + com.google.code.findbugs + + + j2objc-annotations + com.google.j2objc + + + + + + org.freemarker + freemarker + 2.3.29 + + + + com.mashape.unirest + unirest-java + 1.4.9 + + + com.sonic.cow + sonic-cow-lib + 1.0-SNAPSHOT + compile + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pl.project13.maven + git-commit-id-plugin + + + false + false + + + + + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/MainApplication.java b/sonic-cow/server/src/main/java/com/sonic/cow/MainApplication.java new file mode 100644 index 0000000..27e81da --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/MainApplication.java @@ -0,0 +1,24 @@ +package com.sonic.cow; + +import com.sonic.sdk.api.annotation.EnableDecrypt; +import com.sonic.sdk.api.annotation.EnableGatWayAuthScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableScheduling; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * @author chenjun + */ +@EnableSwagger2 +@EnableScheduling +@ComponentScan(value = {"com.sonic"}) +@SpringBootApplication +@EnableGatWayAuthScan(basePackages = "com.sonic.cow.controller") +@EnableDecrypt +public class MainApplication { + public static void main(String[] args) { + SpringApplication.run(MainApplication.class, args); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/AsrClient.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/AsrClient.java new file mode 100644 index 0000000..4e596ca --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/AsrClient.java @@ -0,0 +1,18 @@ +package com.sonic.cow.client; + +import com.sonic.cow.domain.input.VoiceAsrInput; + +/** + * 语音识别(语音转文本) + */ +public interface AsrClient { + + /** + * 语音识别 + * @param userId 用户ID + * @param input base64编码音频内容 + * @return + */ + String asr(String userId, VoiceAsrInput input); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/AsrV2Client.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/AsrV2Client.java new file mode 100644 index 0000000..597ade8 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/AsrV2Client.java @@ -0,0 +1,20 @@ +package com.sonic.cow.client; + +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.domain.input.VoiceAsrInput; +import com.sonic.cow.domain.output.AsrOutput; + +/** + * 语音识别(语音转文本) + */ +public interface AsrV2Client { + + /** + * 语音识别 + * @param userId 用户ID + * @param input base64编码音频内容 + * @return + */ + AsrOutput asr(String userId, VoiceAsrInput input) throws UnirestException, InterruptedException; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/ChatStructuredOutputsClient.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/ChatStructuredOutputsClient.java new file mode 100644 index 0000000..f02a78d --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/ChatStructuredOutputsClient.java @@ -0,0 +1,41 @@ +package com.sonic.cow.client; + +import com.byteplus.ark.runtime.model.completion.chat.ChatCompletionRequest; + +/** + * 结构化输出 + */ +public interface ChatStructuredOutputsClient { + + /** + * 结构化输出 + * @param message + * @param responseFormat + * @return + * @throws Exception + */ + String chatStructuredOutputs(String message, ChatCompletionRequest.ChatCompletionRequestResponseFormat responseFormat); + + + /** + * AI一键生成 + * @param systemPrompt 系统提示词 + * @param temperature 温度 + * @param responseFormat 输出格式 + * @return + */ + String aiGenStructuredOutputs(String systemPrompt, Double temperature, ChatCompletionRequest.ChatCompletionRequestResponseFormat responseFormat,Boolean thinking); + + /** + * 图片生成文本 + * + * 示例: https://github.com/volcengine/volcengine-java-sdk/blob/master/volcengine-java-sdk-ark-runtime/test/java/com/volcengine/ark/runtime/ChatCompletionsVisionExample.java + * + * @param systemPrompt 系统提示词 + * @param temperature 温度 + * @param imageUrl 图片地址 + * @param responseFormat 输出格式 + * @return + */ + String imageGenText(String systemPrompt, Double temperature, String imageUrl, ChatCompletionRequest.ChatCompletionRequestResponseFormat responseFormat); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/ContextChatClient.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/ContextChatClient.java new file mode 100644 index 0000000..770c4cf --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/ContextChatClient.java @@ -0,0 +1,129 @@ +package com.sonic.cow.client; + +import com.byteplus.ark.runtime.model.completion.chat.ChatCompletionChunk; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessage; +import com.byteplus.ark.runtime.model.completion.chat.ChatTool; +import com.sonic.cow.client.request.ChatCompletionMessage; +import com.sonic.cow.domain.bo.AiGen4Bo; +import reactor.core.publisher.Flux; + +import java.util.List; + +/** + * 上下文聊天 + */ +public interface ContextChatClient { + + /** + * 创建上下文缓存 + * @param messages 消息列表 + * @param ttl 有效时间 + * @return 上下文缓存ID + */ + String createContextCaching(List messages, Integer ttl); + + /** + * 上下文文本聊天 + * @param message + * @return + */ + String contextChat(String contextId, String message); + + /** + * 上下文文本聊天 + * @param contextId + * @param message + * @param tools + * @return + */ + String contextChat(String contextId, String message, List tools); + + /** + * 多模态 上下文文本聊天 + * @param contextId + * @param message + * @param tools + * @return + */ + String contextToolsChat(String contextId, String message, List tools); + + /** + * 多模态 上下文文本聊天 + * @param contextId + * @param message + * @param imageUrl + * @return + */ + String contextChat(String contextId, String message, String imageUrl); + + /** + * 聊天 + * @param messages + * @return + */ + String chat(List messages); + + /** + * 流式上下文文本聊天 + * @param message + * @return + */ + Flux streamContextChat(String contextId, String message); + + /** + * AI一键生成 + * @param systemPrompt 系统提示词 + * @param temperature 温度 + * @return + */ + String aiGen(String systemPrompt, Double temperature); + + /** + * AI一键生成 + * @param systemPrompt + * @param temperature + * @return + */ + String aiGen2(String systemPrompt, Double temperature); + + /** + * AI一键生成 + * @param systemPrompt + * @param temperature + * @param topp + * @return + */ + String aiGen3(String systemPrompt, String model, Double temperature, Double topp); + + /** + * AI一键生成 + * @param systemPrompt + * @param temperature + * @param topp + * @return + */ + AiGen4Bo aiGen4(String systemPrompt, String model, Double temperature, Double topp); + + /** + * AI一键生成 + * @param chatMessageList + * @param temperature + * @return + */ + String aiGen4(List chatMessageList, Double temperature); + + + /** + * 敏感词校验 + * @param content + * @return + */ + void nsfwCheck(String content); + + /** + * 敏感词校验 + * @param userContent + * @return + */ + String nsfwCheckV2(String userContent); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/DeepSeekClient.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/DeepSeekClient.java new file mode 100644 index 0000000..6347065 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/DeepSeekClient.java @@ -0,0 +1,16 @@ +package com.sonic.cow.client; + +/** + * DeepSeek + */ +public interface DeepSeekClient { + + /** + * AI一键生成 + * @param systemPrompt 系统提示词 + * @param temperature 温度 + * @return + */ + String aiGen(String systemPrompt, Double temperature); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/ImageGenImageClient.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/ImageGenImageClient.java new file mode 100644 index 0000000..f828ed8 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/ImageGenImageClient.java @@ -0,0 +1,43 @@ +package com.sonic.cow.client; + +public interface ImageGenImageClient { + + /** + * 同步-图生图 人像写真 + * + * @param imageUrl 基图 + * @param prompt 提示词 + * @return + */ + String imageGenImage(String imageUrl, String prompt) throws Exception; + + /** + * 同步-图生图 角色特征保持 + * + * @param imageUrl 基图 + * @param prompt 提示词 + * @return + */ + String imageGenImageV2(String imageUrl, String prompt) throws Exception; + + + + + /** + * 异步-图生图提交任务 + * + * @param imageUrl 基图 + * @param prompt 提示词 + * @return + */ + String imageGenImageSubmitTask(String imageUrl, String prompt); + + /** + * 异步-图生图提交任务 + * + * @param taskId 任务id + * @return + */ + String imageGenImageGetTaskResult(String taskId); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/ResponseChatClient.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/ResponseChatClient.java new file mode 100644 index 0000000..c477de6 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/ResponseChatClient.java @@ -0,0 +1,60 @@ +package com.sonic.cow.client; + +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.client.request.Text; +import com.sonic.cow.client.request.Tools; +import com.sonic.cow.client.response.ResponseChatResponse; + +import java.util.List; + +/** + * 上下文聊天 + */ +public interface ResponseChatClient { + + /** + * 上下文 + 文本 + 图片 + 工具 聊天 + * @param responseId + * @param instructions + * @param inputRequests + * @param tools + * @return + */ + ResponseChatResponse responseStructuredChat(String responseId, String instructions, List inputRequests, List tools, String functionParam, Float temperature, Float topp); + + + /** + * 上下文 + 文本 + 图片 + 工具 聊天 + * @param responseId + * @param instructions + * @param inputRequests + * @param tools + * @return + */ + ResponseChatResponse responseStructuredChat(String responseId, String instructions, List inputRequests, List tools, boolean caching); + + /** + * 上下文 + 文本 + 图片 + 工具 聊天 + * @param responseId + * @param instructions + * @param inputRequests + * @param tools + * @param text + * @param caching + * @return + */ + ResponseChatResponse responseChat(String responseId, String instructions, List inputRequests, List tools, Text text, boolean caching); + + /** + * 上下文 + 文本 + 图片 + 工具 聊天 + * @param responseId + * @param instructions + * @param inputRequests + * @param tools + * @param text + * @param caching + * @return + */ + ResponseChatResponse responseChatV2(String responseId, String instructions, List inputRequests, List tools, Text text, boolean caching, String functionParam, Float temperature, Float topp); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/Seedream4GenImageClient.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/Seedream4GenImageClient.java new file mode 100644 index 0000000..7a427b9 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/Seedream4GenImageClient.java @@ -0,0 +1,14 @@ +package com.sonic.cow.client; + +public interface Seedream4GenImageClient { + + /** + * 生图 + * 可以文生图、图生图 + * + * @param imageUrl + * @param prompt + * @return + */ + String genImage(String imageUrl, String prompt) throws Exception; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/TextGenImageClient.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/TextGenImageClient.java new file mode 100644 index 0000000..6b66b58 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/TextGenImageClient.java @@ -0,0 +1,12 @@ +package com.sonic.cow.client; + +public interface TextGenImageClient { + + /** + * 文生图 + * + * @param prompt + * @return + */ + String textGenImage(String prompt) throws Exception; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsClient.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsClient.java new file mode 100644 index 0000000..95209f8 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsClient.java @@ -0,0 +1,19 @@ +package com.sonic.cow.client; + +import com.sonic.cow.domain.input.VoiceTtsInput; + +/** + * 语音合成(将文本生成语音) + */ +public interface TtsClient { + + + /** + * 语音生成 + * @param userId + * @param input + * @return + */ + String tts(String userId, VoiceTtsInput input); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsHttpStream.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsHttpStream.java new file mode 100644 index 0000000..20ed43a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsHttpStream.java @@ -0,0 +1,198 @@ +package com.sonic.cow.client; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import okhttp3.*; +import org.apache.commons.codec.binary.Base64; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class TtsHttpStream { + // ------------- 客户需要填写参数 ---------------- + private static final String APP_ID = "7330233595"; + private static final String ACCESS_KEY = "NxFxll0gDGl3W0TWW1Fqh23PzKaaQiLu"; + private static final String RESOURCE_ID = "volc.megatts.default"; + private static final String TEXT = "你好我有一个帽衫"; + + // --------------- 请求 URL ---------------------- + private static final String URL = "https://voice.ap-southeast-1.bytepluses.com/api/v3/tts/unidirectional"; + + // Gson 实例用于 JSON 解析 + private static final Gson GSON = new Gson(); + + // OkHttpClient:共享实例,支持复用 + private static final OkHttpClient CLIENT = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build(); + + // 并发控制:Semaphore 限制最大并发数(根据配额调整,例如 1 为安全) + private static final Semaphore SEMAPHORE = new Semaphore(1, true); // 最大 1 个并发 + + // 并发错误码 + private static final int CONCURRENCY_ERROR_CODE = 45000292; + + public static void main(String[] args) throws InterruptedException { + int totalRequests = 18; + for (int i = 1; i <= totalRequests; i++) { + System.out.println("========== 第" + i + "次请求开始 =========="); + ttsHttpStream(i); + System.out.println("========== 第" + i + "次请求结束 =========="); + Thread.sleep(2000); // 每请求后延时 2s,避免率限 + } + } + + public static void ttsHttpStream(int requestId) { + // 获取信号量,确保不超过并发限 + try { + SEMAPHORE.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + Response response = null; + int retryCount = 0; + int maxRetries = 3; + boolean success = false; + + while (retryCount < maxRetries && !success) { + try { + // 构建 headers(可选:添加 "Connection", "close" 强制关闭连接) + Headers.Builder headersBuilder = new Headers.Builder() + .add("X-Api-App-Id", APP_ID) + .add("X-Api-Access-Key", ACCESS_KEY) + .add("X-Api-Resource-Id", RESOURCE_ID) + .add("X-Api-App-Key", "aGjiRDfUWi") + .add("Content-Type", "application/json"); + // .add("Connection", "close"); // 取消注释以强制每次新建连接(测试用) + + Headers headers = headersBuilder.build(); + + // 构建 additions 和 payload(同原代码) + Map additions = new HashMap<>(); + additions.put("disable_markdown_filter", true); + additions.put("enable_language_detector", true); + additions.put("enable_latex_tn", true); + additions.put("disable_default_bit_rate", true); + additions.put("max_length_to_filter_parenthesis", 0); + Map cacheConfig = new HashMap<>(); + cacheConfig.put("text_type", 1); + cacheConfig.put("use_cache", true); + additions.put("cache_config", cacheConfig); + String additionsJson = GSON.toJson(additions); + + Map payload = new HashMap<>(); + Map user = new HashMap<>(); + user.put("uid", "12345"); + payload.put("user", user); + Map reqParams = new HashMap<>(); + reqParams.put("text", TEXT); + reqParams.put("speaker", "S_vbdl9fqD1"); + reqParams.put("additions", additionsJson); + Map audioParams = new HashMap<>(); + audioParams.put("format", "mp3"); + audioParams.put("sample_rate", 24000); + reqParams.put("audio_params", audioParams); + payload.put("req_params", reqParams); + String payloadJson = GSON.toJson(payload); + + RequestBody requestBody = RequestBody.create( + payloadJson, MediaType.get("application/json; charset=utf-8")); + + Request request = new Request.Builder() + .url(URL) + .headers(headers) + .post(requestBody) + .build(); + + response = CLIENT.newCall(request).execute(); + + if (!response.isSuccessful()) { + System.out.println("HTTP 错误: " + response); + retryCount++; + continue; + } + + // 流式读取(同原代码) + ByteArrayOutputStream audioData = new ByteArrayOutputStream(); + long totalAudioSize = 0; + boolean hasError = false; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream()))) { + String chunk; + while ((chunk = reader.readLine()) != null) { + if (chunk.isEmpty()) continue; + System.out.println("json data: " + chunk); + JsonObject data = GSON.fromJson(chunk, JsonObject.class); + int code = data.has("code") ? data.get("code").getAsInt() : 0; + + if (code == 0 && data.has("data") && !data.get("data").isJsonNull()) { + String base64Audio = data.get("data").getAsString(); + byte[] chunkAudio = Base64.decodeBase64(base64Audio); + totalAudioSize += chunkAudio.length; + audioData.write(chunkAudio); + } else if (code == CONCURRENCY_ERROR_CODE) { + System.out.println("并发配额超限,重试中... (第 " + (retryCount + 1) + " 次)"); + hasError = true; + break; + } else if (code == 20000000) { + break; + } else if (code > 0) { + System.out.println("error response: " + data); + hasError = true; + break; + } + } + } + + // 如果无错误,保存文件 + if (!hasError) { + byte[] finalAudio = audioData.toByteArray(); + if (finalAudio.length > 0) { + String outputDir = "tts"; + Files.createDirectories(Paths.get(outputDir)); + String outputFile = Paths.get(outputDir, "tts_test" + requestId + ".mp3").toString(); + try (FileOutputStream fos = new FileOutputStream(outputFile)) { + fos.write(finalAudio); + } + System.out.printf("文件大小: %.2f KB%n", finalAudio.length / 1024.0); + success = true; + } + } else { + retryCount++; + } + + // 重试延时:指数退避 + if (!success && retryCount < maxRetries) { + long backoff = (long) Math.pow(2, retryCount) * 1000; // 2s, 4s, 8s + System.out.println("等待 " + backoff + "ms 后重试..."); + Thread.sleep(backoff); + } + + } catch (Exception e) { + System.out.println("请求异常: " + e.getMessage()); + e.printStackTrace(); + retryCount++; + } finally { + if (response != null) { + response.close(); + } + } + } + + // 释放信号量 + SEMAPHORE.release(); + + if (!success) { + System.out.println("请求 " + requestId + " 最终失败,已达最大重试次数。请检查配额。"); + } + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsHttpStreamReuse.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsHttpStreamReuse.java new file mode 100644 index 0000000..3640901 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsHttpStreamReuse.java @@ -0,0 +1,179 @@ +package com.sonic.cow.client; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import okhttp3.*; +import org.apache.commons.codec.binary.Base64; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class TtsHttpStreamReuse { + // ------------- 客户需要填写参数 ---------------- (同前) + private static final String APP_ID = "7330233595"; + private static final String ACCESS_KEY = "NxFxll0gDGl3W0TWW1Fqh23PzKaaQiLu"; + private static final String RESOURCE_ID = "volc.megatts.default"; + private static final String TEXT = "你好我有一个帽衫"; + private static final String URL = "https://voice.ap-southeast-1.bytepluses.com/api/v3/tts/unidirectional"; + private static final Gson GSON = new Gson(); + + // 共享的OkHttpClient:这是复用关键!默认连接池支持5个空闲连接/主机,keep-alive 5min(>服务器1min) + private static final OkHttpClient CLIENT = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + // 可选:自定义连接池,最大空闲连接5,keep-alive 2min(匹配服务器) + // .connectionPool(new ConnectionPool(5, 2, TimeUnit.MINUTES)) + .build(); + + private static final Semaphore SEMAPHORE = new Semaphore(1, true); // 限并发1 + private static final int CONCURRENCY_ERROR_CODE = 45000292; + + public static void main(String[] args) throws InterruptedException { + int totalRequests = 18; + for (int i = 1; i <= totalRequests; i++) { + System.out.println("========== 第" + i + "次请求开始 =========="); + ttsHttpStream(i); + System.out.println("========== 第" + i + "次请求结束 =========="); + Thread.sleep(2000); // 2s延时,确保<1min复用 + } + // 测试结束后,可选:CLIENT.dispatcher().executorService().shutdown(); // 但不强制 + } + + public static void ttsHttpStream(int requestId) { + try { + SEMAPHORE.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + Response response = null; + int retryCount = 0; + int maxRetries = 3; + boolean success = false; + + while (retryCount < maxRetries && !success) { + try { + // Headers:添加 keep-alive(默认,但显式) + Headers headers = new Headers.Builder() + .add("X-Api-App-Id", APP_ID) + .add("X-Api-Access-Key", ACCESS_KEY) + .add("X-Api-Resource-Id", RESOURCE_ID) + .add("X-Api-App-Key", "aGjiRDfUWi") + .add("Content-Type", "application/json") + .add("Connection", "keep-alive") // 关键:启用复用 + .build(); + + // Payload构建(同前,省略...) + Map additions = new HashMap<>(); + additions.put("disable_markdown_filter", true); + additions.put("enable_language_detector", true); + additions.put("enable_latex_tn", true); + additions.put("disable_default_bit_rate", true); + additions.put("max_length_to_filter_parenthesis", 0); + Map cacheConfig = new HashMap<>(); + cacheConfig.put("text_type", 1); + cacheConfig.put("use_cache", true); + additions.put("cache_config", cacheConfig); + String additionsJson = GSON.toJson(additions); + + Map payload = new HashMap<>(); + Map user = new HashMap<>(); + user.put("uid", "12345"); + payload.put("user", user); + Map reqParams = new HashMap<>(); + reqParams.put("text", TEXT); + reqParams.put("speaker", "S_vbdl9fqD1"); + reqParams.put("additions", additionsJson); + Map audioParams = new HashMap<>(); + audioParams.put("format", "mp3"); + audioParams.put("sample_rate", 24000); + reqParams.put("audio_params", audioParams); + payload.put("req_params", reqParams); + String payloadJson = GSON.toJson(payload); + + RequestBody requestBody = RequestBody.create(payloadJson, MediaType.get("application/json; charset=utf-8")); + Request request = new Request.Builder().url(URL).headers(headers).post(requestBody).build(); + + // 用共享CLIENT执行:这里会自动检查/复用池中连接 + response = CLIENT.newCall(request).execute(); + + if (!response.isSuccessful()) { + System.out.println("HTTP 错误: " + response); + retryCount++; + continue; + } + + // 流式读取(同前,省略细节...) + ByteArrayOutputStream audioData = new ByteArrayOutputStream(); + boolean hasError = false; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream()))) { + String chunk; + while ((chunk = reader.readLine()) != null) { + if (chunk.isEmpty()) continue; + System.out.println("json data: " + chunk); + JsonObject data = GSON.fromJson(chunk, JsonObject.class); + int code = data.has("code") ? data.get("code").getAsInt() : 0; + + if (code == 0 && data.has("data") && !data.get("data").isJsonNull()) { + String base64Audio = data.get("data").getAsString(); + byte[] chunkAudio = Base64.decodeBase64(base64Audio); + audioData.write(chunkAudio); + } else if (code == CONCURRENCY_ERROR_CODE) { + System.out.println("并发配额超限,重试 (第 " + (retryCount + 1) + " 次)"); + hasError = true; + break; + } else if (code == 20000000) { + break; + } else if (code > 0) { + System.out.println("error response: " + data); + hasError = true; + break; + } + } + } + + if (!hasError) { + byte[] finalAudio = audioData.toByteArray(); + if (finalAudio.length > 0) { + String outputDir = "tts"; + Files.createDirectories(Paths.get(outputDir)); + String outputFile = Paths.get(outputDir, "tts_reuse_" + requestId + ".mp3").toString(); + try (FileOutputStream fos = new FileOutputStream(outputFile)) { + fos.write(finalAudio); + } + System.out.printf("文件大小: %.2f KB (复用连接成功)%n", finalAudio.length / 1024.0); + success = true; + } + } else { + retryCount++; + } + + if (!success && retryCount < maxRetries) { + long backoff = (long) Math.pow(2, retryCount) * 1000; + System.out.println("等待 " + backoff + "ms 后重试..."); + Thread.sleep(backoff); + } + + } catch (Exception e) { + System.out.println("请求异常: " + e.getMessage()); + retryCount++; + } finally { + if (response != null) { + response.close(); // 只关闭响应,不关Client(允许复用) + } + } + } + + SEMAPHORE.release(); + if (!success) { + System.out.println("请求 " + requestId + " 失败,请检查配额。"); + } + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsHttpStreamV2.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsHttpStreamV2.java new file mode 100644 index 0000000..32876c9 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsHttpStreamV2.java @@ -0,0 +1,203 @@ +package com.sonic.cow.client; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import okhttp3.*; +import org.apache.commons.codec.binary.Base64; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class TtsHttpStreamV2 { + // ------------- 客户需要填写参数 ---------------- + private static final String APP_ID = "7330233595"; + private static final String ACCESS_KEY = "NxFxll0gDGl3W0TWW1Fqh23PzKaaQiLu"; + private static final String RESOURCE_ID = "volc.megatts.default"; + private static final String TEXT = "你好我有一个帽衫"; + + // --------------- 请求 URL ---------------------- + private static final String URL = "https://voice.ap-southeast-1.bytepluses.com/api/v3/tts/unidirectional"; + + // Gson 实例用于 JSON 解析 + private static final Gson GSON = new Gson(); + + // 并发控制:Semaphore 限制最大并发数(根据配额调整,例如 1 为安全) + private static final Semaphore SEMAPHORE = new Semaphore(1, true); // 最大 1 个并发 + + // 并发错误码 + private static final int CONCURRENCY_ERROR_CODE = 45000292; + + public static void main(String[] args) throws InterruptedException { + int totalRequests = 20; + for (int i = 1; i <= totalRequests; i++) { + System.out.println("========== 第" + i + "次请求开始 =========="); + ttsHttpStream(i); + System.out.println("========== 第" + i + "次请求结束 =========="); + Thread.sleep(2000); // 每请求后延时 2s,避免率限 + } + } + + public static void ttsHttpStream(int requestId) { + // 为每个请求创建独立的 OkHttpClient,确保不复用连接(强制关闭后无复用) + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build(); + + // 获取信号量,确保不超过并发限 + try { + SEMAPHORE.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + client.dispatcher().executorService().shutdownNow(); // 强制关闭客户端 + return; + } + + Response response = null; + int retryCount = 0; + int maxRetries = 3; + boolean success = false; + + while (retryCount < maxRetries && !success) { + try { + // 构建 headers(添加 "Connection": "close" 进一步确保服务器端关闭连接) + Headers.Builder headersBuilder = new Headers.Builder() + .add("X-Api-App-Id", APP_ID) + .add("X-Api-Access-Key", ACCESS_KEY) + .add("X-Api-Resource-Id", RESOURCE_ID) + .add("X-Api-App-Key", "aGjiRDfUWi") + .add("Content-Type", "application/json") + .add("Connection", "close"); // 强制关闭连接,不 keep-alive + + Headers headers = headersBuilder.build(); + + // 构建 additions 和 payload(同原代码) + Map additions = new HashMap<>(); + additions.put("disable_markdown_filter", true); + additions.put("enable_language_detector", true); + additions.put("enable_latex_tn", true); + additions.put("disable_default_bit_rate", true); + additions.put("max_length_to_filter_parenthesis", 0); + Map cacheConfig = new HashMap<>(); + cacheConfig.put("text_type", 1); + cacheConfig.put("use_cache", true); + additions.put("cache_config", cacheConfig); + String additionsJson = GSON.toJson(additions); + + Map payload = new HashMap<>(); + Map user = new HashMap<>(); + user.put("uid", "12345"); + payload.put("user", user); + Map reqParams = new HashMap<>(); + reqParams.put("text", TEXT); + reqParams.put("speaker", "S_vbdl9fqD1"); + reqParams.put("additions", additionsJson); + Map audioParams = new HashMap<>(); + audioParams.put("format", "mp3"); + audioParams.put("sample_rate", 24000); + reqParams.put("audio_params", audioParams); + payload.put("req_params", reqParams); + String payloadJson = GSON.toJson(payload); + + RequestBody requestBody = RequestBody.create( + payloadJson, MediaType.get("application/json; charset=utf-8")); + + Request request = new Request.Builder() + .url(URL) + .headers(headers) + .post(requestBody) + .build(); + + response = client.newCall(request).execute(); + System.out.println("===> tts response X-Tt-Logid: " + response.headers().get("X-Tt-Logid")); + if (!response.isSuccessful()) { + System.out.println("HTTP 错误: " + response); + retryCount++; + continue; + } + + // 流式读取(同原代码) + ByteArrayOutputStream audioData = new ByteArrayOutputStream(); + long totalAudioSize = 0; + boolean hasError = false; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream()))) { + String chunk; + while ((chunk = reader.readLine()) != null) { + if (chunk.isEmpty()) continue; + System.out.println("json data: " + chunk); + JsonObject data = GSON.fromJson(chunk, JsonObject.class); + int code = data.has("code") ? data.get("code").getAsInt() : 0; + + if (code == 0 && data.has("data") && !data.get("data").isJsonNull()) { + String base64Audio = data.get("data").getAsString(); + byte[] chunkAudio = Base64.decodeBase64(base64Audio); + totalAudioSize += chunkAudio.length; + audioData.write(chunkAudio); + } else if (code == CONCURRENCY_ERROR_CODE) { + System.out.println("并发配额超限,重试中... (第 " + (retryCount + 1) + " 次)"); + hasError = true; + break; + } else if (code == 20000000) { + break; + } else if (code > 0) { + System.out.println("error response: " + data); + hasError = true; + break; + } + } + } + + // 如果无错误,保存文件 + if (!hasError) { + byte[] finalAudio = audioData.toByteArray(); + if (finalAudio.length > 0) { + String outputDir = "tts"; + Files.createDirectories(Paths.get(outputDir)); + String outputFile = Paths.get(outputDir, "tts_test_v2_" + requestId + ".mp3").toString(); + try (FileOutputStream fos = new FileOutputStream(outputFile)) { + fos.write(finalAudio); + } + System.out.printf("文件大小: %.2f KB%n", finalAudio.length / 1024.0); + success = true; + } + } else { + retryCount++; + } + + // 重试延时:指数退避 + if (!success && retryCount < maxRetries) { + long backoff = (long) Math.pow(2, retryCount) * 1000; // 2s, 4s, 8s + System.out.println("等待 " + backoff + "ms 后重试..."); + Thread.sleep(backoff); + } + + } catch (Exception e) { + System.out.println("请求异常: " + e.getMessage()); + e.printStackTrace(); + retryCount++; + } finally { + if (response != null) { + response.close(); + } + } + } + + // 释放信号量 + SEMAPHORE.release(); + + // 强制关闭客户端实例,确保连接不复用 + client.dispatcher().executorService().shutdownNow(); + client.connectionPool().evictAll(); // 驱逐所有连接 + + if (!success) { + System.out.println("请求 " + requestId + " 最终失败,已达最大重试次数。请检查配额。"); + } + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsV2Client.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsV2Client.java new file mode 100644 index 0000000..9925b25 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsV2Client.java @@ -0,0 +1,19 @@ +package com.sonic.cow.client; + +import com.sonic.cow.domain.input.VoiceTtsV2Input; + +/** + * 语音合成(将文本生成语音) + */ +public interface TtsV2Client { + + + /** + * 语音生成 + * @param userId + * @param input + * @return + */ + String tts(String userId, VoiceTtsV2Input input); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsV3Client.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsV3Client.java new file mode 100644 index 0000000..77267f4 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/TtsV3Client.java @@ -0,0 +1,19 @@ +package com.sonic.cow.client; + +import com.sonic.cow.domain.input.VoiceTtsV2Input; + +/** + * 语音合成(将文本生成语音) + */ +public interface TtsV3Client { + + + /** + * 语音生成 + * @param userId + * @param input + * @return + */ + String tts(String userId, VoiceTtsV2Input input); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/VoiceChatClient.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/VoiceChatClient.java new file mode 100644 index 0000000..f944705 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/VoiceChatClient.java @@ -0,0 +1,80 @@ +package com.sonic.cow.client; + +import com.sonic.cow.client.input.voicechat.domain.AgentConfig; +import com.sonic.cow.client.input.voicechat.domain.Audio; +import com.sonic.cow.client.input.voicechat.domain.UserPrompt; +import com.sonic.cow.client.rtc.callback.conversation.Conv; +import com.sonic.cow.client.rtc.callback.rts.Subv; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 实时音视频 语音通话 + * + * @author mzc + */ +public interface VoiceChatClient { + + /** + * 生成RTC token + * 文档:https://www.volcengine.com/docs/6348/70121 + * + * @param roomId 房间id + * @param userId 用户id + * @return + */ + String generateRtcToken(String roomId, String userId); + + /** + * 启动智能体 在实时音视频场景中,你可以调用此接口在房间内引入一个智能体进行 AI 实时交互。 + * 文档:https://www.volcengine.com/docs/6348/1558163 + * + * @param roomId + * @param taskId + * @param ttsAudio + * @param systemPrompt + * @param userPromptList + * @param agentConfig + */ + void startVoiceChat(String roomId, String taskId, Audio ttsAudio, String systemPrompt, List userPromptList, AgentConfig agentConfig); + + /** + * 更新智能体 eg:打断 在实时音视频通话场景中,若你需要对智能体进行操作,比如在智能体进行语音输出时进行打断,可以通过调用此接口实现。 + * + * @param roomId + * @param taskId + */ + void updateVoiceChat(String roomId, String taskId); + + /** + * 停止智能体 在实时音视频通话场景中,若你需要结束智能体的语音聊天服务,可以通过调用此接口实现。 + * + * @param roomId + * @param taskId + */ + void stopVoiceChat(String roomId, String taskId); + + /** + * 服务端回调处理 + */ + void webhook(HttpServletRequest request, HttpServletResponse response); + + /** + * 智能体会话状态回调处理 + * + * @param request + * @param response + */ + Conv conversationStateCallback(HttpServletRequest request, HttpServletResponse response); + + /** + * 智能话实时字幕回调处理 + * + * @param request + * @param response + * @return + */ + Subv rtsCallback(HttpServletRequest request, HttpServletResponse response); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/config/AsrCnConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/config/AsrCnConfig.java new file mode 100644 index 0000000..7d07f7c --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/config/AsrCnConfig.java @@ -0,0 +1,23 @@ +package com.sonic.cow.client.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +public class AsrCnConfig { + + @Value("${volcengine.asr-cn.appId}") + private String asrAppId; + + @Value("${volcengine.asr-cn.accessToken}") + private String asrAccessToken; + + @Value("${volcengine.asr-cn.resourceId}") + private String resourceId; + + @Value("${volcengine.asr-cn.flashUrl}") + private String flashUrl; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/config/AsrConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/config/AsrConfig.java new file mode 100644 index 0000000..2c23415 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/config/AsrConfig.java @@ -0,0 +1,23 @@ +package com.sonic.cow.client.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +public class AsrConfig { + + @Value("${volcengine.asr.appId}") + private String asrAppId; + + @Value("${volcengine.asr.accessToken}") + private String asrAccessToken; + + @Value("${volcengine.asr.resourceId}") + private String resourceId; + + @Value("${volcengine.asr.flashUrl}") + private String flashUrl; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/config/TtsCnConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/config/TtsCnConfig.java new file mode 100644 index 0000000..2cacff0 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/config/TtsCnConfig.java @@ -0,0 +1,22 @@ +package com.sonic.cow.client.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +public class TtsCnConfig { + + @Value("${volcengine.tts-cn.appId}") + private String ttsAppId; + @Value("${volcengine.tts-cn.accessToken}") + private String ttsAccessToken; + @Value("${volcengine.tts-cn.provider}") + private String ttsProvider; + @Value("${volcengine.tts-cn.cluster}") + private String cluster; + @Value("${volcengine.tts-cn.ttsUrl}") + private String ttsUrl; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/config/TtsConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/config/TtsConfig.java new file mode 100644 index 0000000..2bd628a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/config/TtsConfig.java @@ -0,0 +1,22 @@ +package com.sonic.cow.client.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +public class TtsConfig { + + @Value("${volcengine.tts.appId}") + private String ttsAppId; + @Value("${volcengine.tts.accessToken}") + private String ttsAccessToken; + @Value("${volcengine.tts.provider}") + private String ttsProvider; + @Value("${volcengine.tts.cluster}") + private String cluster; + @Value("${volcengine.tts.ttsUrl}") + private String ttsUrl; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/AsrClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/AsrClientImpl.java new file mode 100644 index 0000000..613ebdc --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/AsrClientImpl.java @@ -0,0 +1,70 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSONObject; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.JsonNode; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.client.AsrClient; +import com.sonic.cow.client.config.AsrCnConfig; +import com.sonic.cow.client.input.asr.AsrInput; +import com.sonic.cow.domain.input.VoiceAsrInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Slf4j +@Service +public class AsrClientImpl implements AsrClient { + + @Autowired + private AsrCnConfig asrCnConfig; + + @Override + public String asr(String userId, VoiceAsrInput input) { + AsrInput asrInput = new AsrInput(userId, input.getData()); + try { + //RPC 语音转文本 + HttpResponse result = Unirest.post(asrCnConfig.getFlashUrl()) + .header("content-type", "application/json") + .header("X-Api-App-Key", asrCnConfig.getAsrAppId()) + .header("X-Api-Access-Key", asrCnConfig.getAsrAccessToken()) + .header("X-Api-Resource-Id", asrCnConfig.getResourceId()) + .header("X-Api-Request-Id", UUID.randomUUID().toString()) + .header("X-Api-Sequence", "-1") + .body(JSONObject.toJSONString(asrInput)) + .asString(); + String body = result.getBody(); + log.info("===> asr body : {}", body); + JSONObject jsonObject = JSONObject.parseObject(body); + return jsonObject.getJSONObject("result").getString("text"); + } catch (UnirestException e) { + log.error("===> asr error : ", e); + } + return ""; + } + + public static void main(String[] args) { + try { + // POST 请求到 OpenAI Whisper 转录端点 + HttpResponse response = Unirest.post("https://api.openai.com/v1/audio/transcriptions") + .header("Authorization", "Bearer " + "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") + .header("Content-Type", "multipart/form-data") + .field("model", "gpt-4o-mini-transcribe") + .field("file", "https://sound-oss.epal.gg/data/sound/546963/17005091327548797.mp3") // 上传音频文件 + .asJson(); + + // 检查响应状态 + if (response.getStatus() == 200) { + JsonNode body = response.getBody(); + System.out.println("转录结果: " + body.getObject().get("text").toString()); + } else { + System.err.println("错误: " + response.getStatus() + " - " + response.getBody()); + } + } catch (UnirestException e) { + e.printStackTrace(); + } + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/AsrV2ClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/AsrV2ClientImpl.java new file mode 100644 index 0000000..69c4e47 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/AsrV2ClientImpl.java @@ -0,0 +1,171 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.client.AsrV2Client; +import com.sonic.cow.client.config.AsrConfig; +import com.sonic.cow.domain.input.VoiceAsrInput; +import com.sonic.cow.domain.output.AsrOutput; +import com.sonic.cow.enums.ToastResultCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Slf4j +@Service +public class AsrV2ClientImpl implements AsrV2Client { + + @Autowired + private AsrConfig asrConfig; + + /** + * 提交地址 + */ + private static final String submitUrl = "https://voice.ap-southeast-1.bytepluses.com/api/v3/auc/bigmodel/submit"; + /** + * 查询地址 + */ + private static final String queryUrl = "https://voice.ap-southeast-1.bytepluses.com/api/v3/auc/bigmodel/query"; + /** + * 资源ID + */ + private static final String resourceId = "volc.bigasr.auc"; + /** + * 状态码 + */ + private static final String X_API_STATUS_CODE = "X-Api-Status-Code"; + + + @Override + public AsrOutput asr(String userId, VoiceAsrInput input) throws UnirestException, InterruptedException { + //TODO 判断url的格式是否有效(host是否有效),判断是否是用户自己的 + + String taskId = UUID.randomUUID().toString(); + //STEP1: 创建语音识别任务 + String xttLogId = submitTask(userId, taskId, input.getUrl()); + //没有创建成功,直接抛出异常 + ToastResultCode.SOUND_ASR_ERROR.check(StringUtils.isEmpty(xttLogId)); + int count = 0; + //STEP2 获取结果 + while (true) { + //记录处理次数,处理次数达到35次,也就是10s都没有结果返回的话那么久不再继续处理了 + if(count > 35) { + break; + } + HttpResponse queryResponse = queryTask(taskId, xttLogId); + String code = queryResponse.getHeaders().getFirst(X_API_STATUS_CODE); + if ("20000000".equals(code)) { + JSONObject jsonObject = JSONObject.parseObject(queryResponse.getBody()); + log.info("=======> asr jsonObject:{}", JSONObject.toJSONString(jsonObject)); + JSONObject result = jsonObject.getJSONObject("result"); + JSONArray utterances = result.getJSONArray("utterances"); + JSONObject firstUtterance = utterances.getJSONObject(0); + String text = firstUtterance.getString("text"); + return AsrOutput.builder() + .content(text) + .duration(jsonObject.getJSONObject("audio_info").getInteger("duration")) + .build(); + } else if (!"20000001".equals(code) && !"20000002".equals(code)) { + //不为队列中或处理中的状态抛出异常 + ToastResultCode.SOUND_ASR_ERROR.check(true); + } + //休眠0.3秒后再次请求获取结果 + Thread.sleep(300); + //计数器增加 + count++; + } + return null; + } + + /** + * 创建任务 + * @param taskId + * @param fileUrl + * @return + * @throws UnirestException + */ + private String submitTask(String userId, String taskId, String fileUrl) throws UnirestException { + JSONObject requestBody = new JSONObject(); + JSONObject user = new JSONObject(); + user.put("uid", "fake_uid"); + requestBody.put("user", user); + JSONObject audio = new JSONObject(); + audio.put("url", fileUrl); + requestBody.put("audio", audio); + JSONObject request = new JSONObject(); + request.put("model_name", "bigmodel"); + request.put("enable_channel_split", true); + request.put("enable_ddc", true); + request.put("enable_speaker_info", true); + request.put("enable_punc", true); + request.put("enable_itn", true); + JSONObject corpus = new JSONObject(); + corpus.put("correct_table_name", ""); + corpus.put("context", ""); + request.put("corpus", corpus); + requestBody.put("request", request); + + log.info("===> Submit task request body: {}", requestBody); + + HttpResponse response = Unirest.post(submitUrl) + .header("X-Api-App-Key", asrConfig.getAsrAppId()) + .header("X-Api-Access-Key", asrConfig.getAsrAccessToken()) + .header("X-Api-Resource-Id", resourceId) + .header("X-Api-Request-Id", taskId) + .header("X-Api-Sequence", "-1") + .body(requestBody.toString()) + .asString(); + + String statusCode = response.getHeaders().getFirst(X_API_STATUS_CODE); + String logId = response.getHeaders().getFirst("X-Tt-Logid"); + log.info("===> Submit task success statusCode: {}, logId : {}", statusCode, logId); + if ("20000000".equals(statusCode)) { + return logId; + } else { + log.info("===> Submit task failed and the response headers are: {}", response.getHeaders()); + } + return null; + } + + /** + * 查询任务 + * @param taskId + * @param xTtLogid + * @return + * @throws UnirestException + */ + private HttpResponse queryTask(String taskId, String xTtLogid) throws UnirestException { + HttpResponse response = Unirest.post(queryUrl) + .header("X-Api-App-Key", asrConfig.getAsrAppId()) + .header("X-Api-Access-Key", asrConfig.getAsrAccessToken()) + .header("X-Api-Resource-Id", resourceId) + .header("X-Api-Request-Id", taskId) + .header("X-Tt-Logid", xTtLogid) + .body("{}") + .asString(); + + //查询结果代码说明: +// 20000000 Success +// 20000001 Processing +// 20000002 Task in queue +// 20000003 Mute audio (There is no need to query again when this error code is returned. Just submit again directly.) +// 45000001 Invalid request parameters (The request parameters are missing required fields / the field values are invalid / repeated requests.) +// 45000002 Empty audio +// 45000151 Incorrect audio format +// 550xxxx Service internal processing error +// 55000031 Server busy (The service is overloaded and cannot process the current request.) + if (response.getHeaders().containsKey("X-Api-Status-Code")) { + log.info("===> Query task response header X-Api-Status-Code: {}", response.getHeaders().getFirst("X-Api-Status-Code")); + } else { + log.info("===> Query task failed and the response headers are: {}", response.getHeaders()); + } + return response; + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ChatStructuredOutputsClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ChatStructuredOutputsClientImpl.java new file mode 100644 index 0000000..f0de02c --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ChatStructuredOutputsClientImpl.java @@ -0,0 +1,116 @@ +package com.sonic.cow.client.impl; + +import com.google.common.collect.Lists; +import com.sonic.cow.client.ChatStructuredOutputsClient; +import com.byteplus.ark.runtime.model.completion.chat.ChatCompletionContentPart; +import com.byteplus.ark.runtime.model.completion.chat.ChatCompletionRequest; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessage; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import com.byteplus.ark.runtime.service.ArkService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class ChatStructuredOutputsClientImpl implements ChatStructuredOutputsClient { + + @Value("${volcengine.contextCachingModel}") + private String contextCachingModel; + + @Autowired + private ArkService volcengineArkService; + + @Override + public String chatStructuredOutputs(String message, ChatCompletionRequest.ChatCompletionRequestResponseFormat responseFormat) { + List messages = Lists.newArrayList(); + messages.add(ChatMessage.builder() + .role(ChatMessageRole.USER) + .content(message) + .build()); + //在结构化输出中,如果关闭了推理的话是无法结构化输出内容的 + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(contextCachingModel) + .messages(messages) + .responseFormat(responseFormat) + .build(); + StringBuffer stringBuffer = new StringBuffer(); + Long startTime = System.currentTimeMillis(); + volcengineArkService.createChatCompletion(chatCompletionRequest).getChoices().forEach( + choice -> { + stringBuffer.append(choice.getMessage().getContent()); + } + ); + Long endTime = System.currentTimeMillis(); + log.info("chatStructuredOutputs 所需要的时间:{}" , (endTime - startTime) / 1000); + return stringBuffer.toString(); + } + + @Override + public String aiGenStructuredOutputs(String systemPrompt, Double temperature, ChatCompletionRequest.ChatCompletionRequestResponseFormat responseFormat, Boolean thinking) { + log.info("aiGenStructuredOutputs systemPrompt : {}", systemPrompt); + List messages = Lists.newArrayList(); + messages.add(ChatMessage.builder() + .role(ChatMessageRole.USER) + .content(systemPrompt) + .build()); + //在结构化输出中,如果关闭了推理的话是无法结构化输出内容的 + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(contextCachingModel) + .messages(messages) + .temperature(temperature) + .responseFormat(responseFormat) +// .thinking(thinking != null && thinking ? new ChatCompletionRequest.ChatCompletionRequestThinking("enabled") : new ChatCompletionRequest.ChatCompletionRequestThinking("disabled")) + .build(); + StringBuffer stringBuffer = new StringBuffer(); + + Long startTime = System.currentTimeMillis(); + volcengineArkService.createChatCompletion(chatCompletionRequest).getChoices().forEach( + choice -> { + stringBuffer.append(choice.getMessage().getContent()); + log.info("===> ReasoningContent : {}", choice.getMessage().getReasoningContent()); + } + ); + Long endTime = System.currentTimeMillis(); + log.info("aiGenStructuredOutputs time : {}", (endTime - startTime) / 1000); + return stringBuffer.toString(); + } + + @Override + public String imageGenText(String systemPrompt, Double temperature, String imageUrl, ChatCompletionRequest.ChatCompletionRequestResponseFormat responseFormat) { + log.info("图生文系统提示词:{}" , systemPrompt); + final List messages = new ArrayList<>(); + messages.add(ChatMessage.builder() + .role(ChatMessageRole.SYSTEM) + .content(systemPrompt).build()); + final List multiParts = new ArrayList<>(); + multiParts.add(ChatCompletionContentPart.builder().type("image_url").imageUrl( + new ChatCompletionContentPart.ChatCompletionContentPartImageURL( + imageUrl + ) + ).build()); + final ChatMessage userMessage = ChatMessage.builder().role(ChatMessageRole.USER) + .multiContent(multiParts).build(); + messages.add(userMessage); + + Long startTime = System.currentTimeMillis(); + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(contextCachingModel) + .messages(messages) +// .thinking(new ChatCompletionRequest.ChatCompletionRequestThinking("disabled")) + .temperature(temperature == null ? 0.5 : temperature) + .responseFormat(responseFormat) + .build(); + + StringBuffer stringBuffer = new StringBuffer(); + volcengineArkService.createChatCompletion(chatCompletionRequest).getChoices().forEach(choice -> stringBuffer.append(choice.getMessage().getContent())); + + Long endTime = System.currentTimeMillis(); + log.info("图生文所需要的时间:{}" , (endTime - startTime) / 1000); + return stringBuffer.toString(); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ContextChatClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ContextChatClientImpl.java new file mode 100644 index 0000000..196b6ad --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ContextChatClientImpl.java @@ -0,0 +1,599 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.byteplus.ark.runtime.model.completion.chat.*; +import com.byteplus.ark.runtime.model.context.CreateContextRequest; +import com.byteplus.ark.runtime.model.context.CreateContextResult; +import com.byteplus.ark.runtime.model.context.TruncationStrategy; +import com.byteplus.ark.runtime.model.context.chat.ContextChatCompletionRequest; +import com.byteplus.ark.runtime.service.ArkService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Lists; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.client.ContextChatClient; +import com.sonic.cow.client.request.ChatCompletionMessage; +import com.sonic.cow.domain.bo.AiGen4Bo; +import com.sonic.cow.enums.ToastResultCode; +import com.sonic.cow.tools.GetAiImageArgs; +import com.sonic.frog.lib.client.AiUserAlbumClient; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Service +public class ContextChatClientImpl implements ContextChatClient { + + @Value("${volcengine.contextCachingModel}") + private String contextCachingModel; + + @Value("${volcengine.apiKey:4075846b-cb9e-45f6-bf59-ab0a94745d9b}") + private String apiKey = "4075846b-cb9e-45f6-bf59-ab0a94745d9b"; + + @Value("${volcengine.chatUrl:https://ark.ap-southeast.bytepluses.com/api/v3/chat/completions}") + private String chatUrl = "https://ark.ap-southeast.bytepluses.com/api/v3/chat/completions"; + + @Autowired + private ArkService volcengineArkService; + @Autowired + private AiUserAlbumClient aiUserAlbumClient; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String createContextCaching(List messages, Integer ttl) { + CreateContextRequest createContextRequest = CreateContextRequest.builder() + .model(contextCachingModel) + .mode("session") + .truncationStrategy(TruncationStrategy.builder() + .type("rolling_tokens") + .rollingTokens(true) + .build()) + .messages(messages) + //过期时间1小时 + .ttl(ttl) + .build(); + log.info("===> createContextCaching request : {}", JSONObject.toJSONString(createContextRequest)); + CreateContextResult createContextResult = volcengineArkService.createContext(createContextRequest); + String contextId = createContextResult.getId(); + log.info("===> created context, id = {}", contextId); + return contextId; + } + + @Override + public String contextChat(String contextId, String message) { + ContextChatCompletionRequest chatCompletionRequest = ContextChatCompletionRequest.builder() + .contextId(contextId) + .model(contextCachingModel) + .messages(Collections.singletonList(ChatMessage.builder() + .role(ChatMessageRole.USER) + .content(message).build())) + .build(); + ChatCompletionResult chatCompletionResult = volcengineArkService.createContextChatCompletion(chatCompletionRequest); + StringBuffer stringBuffer = new StringBuffer(); + chatCompletionResult.getChoices().forEach(choice -> stringBuffer.append(choice.getMessage().getContent())); + return stringBuffer.toString(); + } + + @Override + public String contextChat(String contextId, String message, List tools) { + ContextChatCompletionRequest chatCompletionRequest = ContextChatCompletionRequest.builder() + .contextId(contextId) + .model(contextCachingModel) + .messages(Collections.singletonList(ChatMessage.builder() + .role(ChatMessageRole.USER) + .content(message).build())) + .tools(tools) + .build(); + ChatCompletionResult chatCompletionResult = volcengineArkService.createContextChatCompletion(chatCompletionRequest); + + ChatCompletionChoice choice1 = chatCompletionResult.getChoices().get(0); + + StringBuffer stringBuffer = new StringBuffer(); + chatCompletionResult.getChoices().forEach(choice -> stringBuffer.append(choice.getMessage().getContent())); + return stringBuffer.toString(); + } + + + @Override + public String contextToolsChat(String contextId, String message, List tools) { + List messages = Lists.newArrayList(); + messages.add(ChatMessage.builder() + .role(ChatMessageRole.USER) + .content(message).build()); + ContextChatCompletionRequest chatCompletionRequest = ContextChatCompletionRequest.builder() + .contextId(contextId) + .model(contextCachingModel) + .messages(messages) + .tools(tools) + .build(); + ChatCompletionResult chatCompletionResult = volcengineArkService.createContextChatCompletion(chatCompletionRequest); + ChatCompletionChoice choice = chatCompletionResult.getChoices().get(0); + List toolCalls = choice.getMessage().getToolCalls(); + ChatMessage feedbackChatMessage = null; + //判断是否需要走本地方法调用 + if (choice.getFinishReason() != null && "tool_calls".equalsIgnoreCase(choice.getFinishReason()) && toolCalls != null && !toolCalls.isEmpty()) { + //执行工具方法的调用 + for (ChatToolCall toolCall : toolCalls) { + String toolName = toolCall.getFunction().getName(); + //处理获取AI未解锁图片的方法调用 + if ("get_ai_image".equals(toolName)) { + //解析入参 + String argumentsJson = toolCall.getFunction().getArguments(); + GetAiImageArgs tool_args = null; + try { + tool_args = objectMapper.readValue(argumentsJson, GetAiImageArgs.class); + } catch (JsonProcessingException e) { + // 将错误信息作为工具结果回填 + feedbackChatMessage = ChatMessage.builder() + .role(ChatMessageRole.TOOL) + .content("解析参数时出错: " + e.getMessage()) + .toolCallId(toolCall.getId()) + .build(); + log.error("===> 解析 send_ai_Image 参数时出错: " + argumentsJson + " - " + e.getMessage()); + } + //调用方法获取待解锁图片 + AIUserAlbumApiOutput toolResult = aiUserAlbumClient.getRandomLockImage(tool_args.getUserId(), tool_args.getAiId()); + log.info("===> 工具执行结果 id : {}, result : {}", toolCall.getId(), toolResult); + if (toolResult == null) { + // 步骤 4:回填工具结果,并获取模型总结回复 + feedbackChatMessage = ChatMessage.builder() + .role(ChatMessageRole.TOOL) + .content("未找到图片") + .toolCallId(toolCall.getId()) // 关联工具调用 ID + .build(); + } else { + //返回 图片 + return toolResult.getImgUrl(); + } + } + } + } + if(feedbackChatMessage != null) { + //TODO 再次调用反馈结果 + + } + StringBuffer stringBuffer = new StringBuffer(); + chatCompletionResult.getChoices().forEach(choice1 -> stringBuffer.append(choice1.getMessage().getContent())); + return stringBuffer.toString(); + } + + @Override + public String contextChat(String contextId, String message, String imageUrl) { + List messages = Lists.newArrayList(); + List multiParts = new ArrayList<>(); + multiParts.add(ChatCompletionContentPart.builder().type("text").text(message).build()); + multiParts.add(ChatCompletionContentPart.builder().type("image_url").imageUrl(new ChatCompletionContentPart.ChatCompletionContentPartImageURL(imageUrl)).build()); + //添加用户发送内容 多模态(文本、图片) + ChatMessage userMessage = ChatMessage.builder() + .role(ChatMessageRole.USER) + .multiContent(multiParts).build(); + messages.add(userMessage); + + log.info("===> contextChat messages : {}", JSONObject.toJSONString(messages)); + + ContextChatCompletionRequest chatCompletionRequest = ContextChatCompletionRequest.builder() + .contextId(contextId) + .model(contextCachingModel) + .messages(messages) + .build(); + ChatCompletionResult chatCompletionResult = volcengineArkService.createContextChatCompletion(chatCompletionRequest); + StringBuffer stringBuffer = new StringBuffer(); + chatCompletionResult.getChoices().forEach(choice -> stringBuffer.append(choice.getMessage().getContent())); + return stringBuffer.toString(); + } + + @Override + public String chat(List messages) { + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(contextCachingModel) + .messages(messages) + .build(); + ChatCompletionResult chatCompletionResult = volcengineArkService.createChatCompletion(chatCompletionRequest); + StringBuffer stringBuffer = new StringBuffer(); + chatCompletionResult.getChoices().forEach(choice -> stringBuffer.append(choice.getMessage().getContent())); + return stringBuffer.toString(); + } + + @Override + public Flux streamContextChat(String contextId, String message) { + ContextChatCompletionRequest streamChatCompletionRequest = ContextChatCompletionRequest.builder() + .contextId(contextId) + .model(contextCachingModel) + .messages(Collections.singletonList(ChatMessage.builder().role(ChatMessageRole.USER).content(message).build())) + .build(); + return Flux.from(volcengineArkService.streamContextChatCompletion(streamChatCompletionRequest)); + } + + @Override + public String aiGen(String systemPrompt, Double temperature) { + log.info("===> aiGen systemPrompt : {}", systemPrompt); + Long startTime = System.currentTimeMillis(); + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(contextCachingModel) + .messages(Collections.singletonList(ChatMessage.builder() + .role(ChatMessageRole.USER) + .content(systemPrompt).build())) +// .thinking(new ChatCompletionRequest.ChatCompletionRequestThinking("disabled")) + .temperature(temperature == null ? 0.5 : temperature) + .build(); + ChatCompletionResult chatCompletionResult = volcengineArkService.createChatCompletion(chatCompletionRequest); + StringBuffer stringBuffer = new StringBuffer(); + chatCompletionResult.getChoices().forEach(choice -> stringBuffer.append(choice.getMessage().getContent())); + Long endTime = System.currentTimeMillis(); + log.info("===> aiGen time : {}", (endTime - startTime) / 1000); + return stringBuffer.toString(); + } + + @Override + public String aiGen2(String systemPrompt, Double temperature) { + try { + Long startTime = System.currentTimeMillis(); + JSONObject requestBody = new JSONObject(); + requestBody.put("model", contextCachingModel); + JSONArray messages = new JSONArray(); + JSONObject message = new JSONObject(); + message.put("role", ChatMessageRole.USER.value()); + message.put("content", systemPrompt); + messages.add(message); + requestBody.put("messages", messages); + JSONObject thinking = new JSONObject(); + thinking.put("type", "disabled"); + requestBody.put("thinking", thinking); + //设置温度值 + if(temperature != null) { + requestBody.put("temperature", temperature); + } + log.info("===> aiGen body : {}", requestBody); + HttpResponse response = Unirest.post(chatUrl) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .asString(); + String result = response.getBody(); + log.info("===> aiGen result : {}", result); + JSONObject jsonObject = JSONObject.parseObject(result); + String content = jsonObject.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content"); + Long endTime = System.currentTimeMillis(); + log.info("===> aiGen time : {}", (endTime - startTime) / 1000); + return content; + } catch (UnirestException e) { + throw new RuntimeException(e); + } + } + + @Override + public String aiGen3(String systemPrompt, String model, Double temperature, Double topp) { + try { + Long startTime = System.currentTimeMillis(); + JSONObject requestBody = new JSONObject(); + if(StringUtils.isNotEmpty(model)) { + requestBody.put("model", model); + } else { + //chatgpt 模型 + requestBody.put("model", "ep-20250917134025-t2hfr"); + //1.6 模型 +// requestBody.put("model", "ep-20250924173624-7r6kb"); + } + JSONArray messages = new JSONArray(); + JSONObject message = new JSONObject(); + message.put("role", ChatMessageRole.USER.value()); + message.put("content", systemPrompt); + messages.add(message); + requestBody.put("messages", messages); + JSONObject thinking = new JSONObject(); + thinking.put("type", "disabled"); + requestBody.put("thinking", thinking); + //设置温度值 + requestBody.put("temperature", temperature != null ? temperature : 1.0f); + //召回率 + requestBody.put("top_p", topp != null ? topp : 0.7f); + log.info("===> aiGen body : {}", requestBody); + HttpResponse response = Unirest.post(chatUrl) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .asString(); + String result = response.getBody(); + log.info("===> aiGen result : {}", result); + JSONObject jsonObject = JSONObject.parseObject(result); + String content = jsonObject.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content"); + Long endTime = System.currentTimeMillis(); + log.info("===> aiGen time : {}", (endTime - startTime) / 1000); + return content; + } catch (UnirestException e) { + throw new RuntimeException(e); + } + } + + @Override + public AiGen4Bo aiGen4(String systemPrompt, String model, Double temperature, Double topp) { + try { + AiGen4Bo aiGen4Bo = new AiGen4Bo(); + Long startTime = System.currentTimeMillis(); + JSONObject requestBody = new JSONObject(); + if(StringUtils.isNotEmpty(model)) { + requestBody.put("model", model); + } else { + //chatgpt 模型 + requestBody.put("model", "ep-20250917134025-t2hfr"); + //1.6 模型 +// requestBody.put("model", "ep-20250924173624-7r6kb"); + } + JSONArray messages = new JSONArray(); + JSONObject systemMessage = new JSONObject(); + systemMessage.put("role", ChatMessageRole.SYSTEM.value()); + systemMessage.put("content", systemPrompt); + messages.add(systemMessage); + + JSONObject userMessage = new JSONObject(); + userMessage.put("role", ChatMessageRole.USER.value()); + userMessage.put("content", "生成"); + messages.add(userMessage); + + requestBody.put("messages", messages); + JSONObject thinking = new JSONObject(); + thinking.put("type", "disabled"); + requestBody.put("thinking", thinking); + //设置温度值 + requestBody.put("temperature", temperature != null ? temperature : 1.0f); + //召回率 + requestBody.put("top_p", topp != null ? topp : 0.7f); + aiGen4Bo.setInput(requestBody.toString()); + log.info("===> aiGen body : {}", requestBody); + HttpResponse response = Unirest.post(chatUrl) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .asString(); + String result = response.getBody(); + aiGen4Bo.setOutput(result); + aiGen4Bo.setUrl(chatUrl); + aiGen4Bo.setApikey(apiKey); + log.info("===> aiGen result : {}", result); + JSONObject jsonObject = JSONObject.parseObject(result); + String content = jsonObject.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content"); + Long endTime = System.currentTimeMillis(); + log.info("===> aiGen time : {}", (endTime - startTime) / 1000); + aiGen4Bo.setContent(content); + return aiGen4Bo; + } catch (UnirestException e) { + throw new RuntimeException(e); + } + } + + /** + * 使用deepseek + * + * @param chatMessageList + * @param temperature + * @return + */ + @Override + public String aiGen4(List chatMessageList, Double temperature) { + try { + Long startTime = System.currentTimeMillis(); + JSONObject requestBody = new JSONObject(); + //使用deepseek + requestBody.put("model", "ep-20250910114251-szxqk"); + requestBody.put("messages", chatMessageList); + JSONObject thinking = new JSONObject(); + thinking.put("type", "disabled"); + requestBody.put("thinking", thinking); + //设置温度值 + if(temperature != null) { + requestBody.put("temperature", temperature); + } + log.info("===> aiGen body : {}", requestBody); + HttpResponse response = Unirest.post(chatUrl) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .asString(); + String result = response.getBody(); + log.info("===> aiGen result : {}", result); + JSONObject jsonObject = JSONObject.parseObject(result); + String content = jsonObject.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content"); + Long endTime = System.currentTimeMillis(); + log.info("===> aiGen time : {}", (endTime - startTime) / 1000); + return content; + } catch (UnirestException e) { + throw new RuntimeException(e); + } + } + + @Override + public void nsfwCheck(String userContent) { + boolean checkPass = true; + String content1 = ""; + try { + String systemPrompt = "你是一位资深的敏感词校验工程师\n" + + "\n" + + "\n" + + "请你校验用户输入的文本内容是否包含违反法律、伦理或社会道德的敏感内容\n" + + "\n" + + "\n" + + "审核时必须符合以下'/guide/' 标签内的审查规则:\n" + + "\n" + + "\n" + + "\n" + + "1. 禁止儿童性虐待材料:任何描绘或暗示未成年人的性行为或性暗示的内容。\n" + + "2. 禁止宣扬恐怖主义:包含恐怖组织标志、宣传材料、恐怖行为实录或指导的内容。\n" + + "3. 禁止描绘现实中存在的名人,公众人物\n" + + "4. 禁止描述真实存在的引人不适的血腥暴力场景:如肢解、斩首、内脏暴露、大规模血腥事故、战争实录的血腥特写等。\n" + + "5.禁止描述残酷的虐待与折磨:对人类或动物进行极端虐待、折磨致残或致死的写实内容。\n" + + "6.禁止描述自残与自杀:描绘或鼓励自残、自杀的写实方法或过程的内容。\n" + + "5. 禁止描述性暴力:描绘强奸、非自愿捆绑、下药等任何非自愿性行为的内容。\n" + + "7.禁止描述性虐待(BDSM中的硬核内容):即使看似“自愿”,也禁止包含极端疼痛、羞辱、可能导致永久身体伤害的行为(如窒息、重度穿刺、伤口性行为等)。\n" + + "8.禁止描述恋物癖与排泄物:禁止涉及粪便(Scatology)、尿液(Urolagnia)、极端身体改造等内容的色情图片。\n" + + "9.禁止描述兽交(Zoophilia):任何人与动物发生性行为的内容。\n" + + "10.禁止直接描绘性行为(性交、自慰等行为动作描述)。禁止描述性玩具的使用。禁止特写性器官。\n" + + "11. 禁止基于种族、民族、宗教、国籍、性别、性取向、残疾等,宣扬仇恨、进行非人化侮辱或煽动暴力的内容。\n" + + "\n" + + "\n" + + "\n" + + "注意,对以下’/whitelist/' 中内容可以放行\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "对于非裸露的身材描述予以放过,即使带有轻度的暗示,例如饱满的胸部,圆润的臀部等\n" + + "\n" + + "对于并基于现实的,属于文学创作的描写,如虚构的犯罪、暴力事件,适度的恐怖,血腥,战争等场景描述\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "输出遵循以下’/output/’中规则:\n" + + "\n" + + "\n" + + "\n" + + "违规命中的敏感词或短句,以json格式输出,示例如下:\n" + + "{\\\"checkPass\\\": false, \\\"content\\”:\\”nude body\\”}\n" + + "\n" + + "\n" + + "字段说明:checkPass布尔值,包含敏感词为false、不包含敏感词为true。content为字符类型,里面填充的是你检测到的所有的敏感词\n" + + "\n" + + "\n" + + ""; + Long startTime = System.currentTimeMillis(); + JSONObject requestBody = new JSONObject(); + requestBody.put("model", "ep-20250917134025-t2hfr"); + JSONArray messages = new JSONArray(); + JSONObject systemMessage = new JSONObject(); + systemMessage.put("role", ChatMessageRole.SYSTEM.value()); + systemMessage.put("content", systemPrompt); + messages.add(systemMessage); + JSONObject userMessage = new JSONObject(); + userMessage.put("role", ChatMessageRole.USER.value()); + userMessage.put("content", userContent); + messages.add(userMessage); + requestBody.put("messages", messages); + JSONObject thinking = new JSONObject(); + thinking.put("type", "disabled"); + requestBody.put("thinking", thinking); + log.info("===> nsfwCheck body : {}", requestBody); + HttpResponse response = Unirest.post("https://ark.ap-southeast.bytepluses.com/api/v3/chat/completions") + .header("Authorization", "Bearer " + "4075846b-cb9e-45f6-bf59-ab0a94745d9b") + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .asString(); + String result = response.getBody(); + log.info("===> nsfwCheck result : {}", result); + JSONObject jsonObject = JSONObject.parseObject(result); + String content = jsonObject.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content"); + Long endTime = System.currentTimeMillis(); + log.info("===> nsfwCheck time : {}", (endTime - startTime) / 1000); + System.out.println(content); + JSONObject jsonObject1 = JSONObject.parseObject(content); + checkPass = jsonObject1.getBoolean("checkPass"); + content1 = jsonObject1.getString("content"); + } catch (UnirestException e) { + throw new RuntimeException(e); + } catch (Exception e) { + log.error("===> nsfwCheck error : ", e); + } + //判断是否抛出异常 + ToastResultCode.NSWF_ERROR.check(!checkPass, String.format(ToastResultCode.NSWF_ERROR.getErrorMsg(), content1)); + } + + @Override + public String nsfwCheckV2(String userContent) { + boolean checkPass = true; + String content1 = ""; + try { + String systemPrompt = "你是一位资深的敏感词校验工程师,请你校验用户输入的文本内容是否包含敏感词汇,除生殖器官用词以外的色情用词都可以认定为不包含敏感词,并将命中的结果以json格式输出,示例如下:{\"checkPass\": false, \"content\":\"fuck\"}" + + "字段说明:checkPass布尔值,包含敏感词为false、不包含敏感词为true。content为字符类型,里面填充的是你检测到的所有的敏感词"; + Long startTime = System.currentTimeMillis(); + JSONObject requestBody = new JSONObject(); + requestBody.put("model", "ep-20250917134025-t2hfr"); + JSONArray messages = new JSONArray(); + JSONObject systemMessage = new JSONObject(); + systemMessage.put("role", ChatMessageRole.SYSTEM.value()); + systemMessage.put("content", systemPrompt); + messages.add(systemMessage); + JSONObject userMessage = new JSONObject(); + userMessage.put("role", ChatMessageRole.USER.value()); + userMessage.put("content", userContent); + messages.add(userMessage); + requestBody.put("messages", messages); + JSONObject thinking = new JSONObject(); + thinking.put("type", "disabled"); + requestBody.put("thinking", thinking); + log.info("===> nsfwCheck body : {}", requestBody); + HttpResponse response = Unirest.post("https://ark.ap-southeast.bytepluses.com/api/v3/chat/completions") + .header("Authorization", "Bearer " + "4075846b-cb9e-45f6-bf59-ab0a94745d9b") + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .asString(); + String result = response.getBody(); + log.info("===> nsfwCheck result : {}", result); + JSONObject jsonObject = JSONObject.parseObject(result); + String content = jsonObject.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content"); + Long endTime = System.currentTimeMillis(); + log.info("===> nsfwCheck time : {}", (endTime - startTime) / 1000); + System.out.println(content); + JSONObject jsonObject1 = JSONObject.parseObject(content); + checkPass = jsonObject1.getBoolean("checkPass"); + content1 = jsonObject1.getString("content"); + } catch (UnirestException e) { + throw new RuntimeException(e); + } catch (Exception e) { + log.error("===> nsfwCheck error : ", e); + } + return content1; + } + + public static void main(String[] args) { + try { + String systemPrompt = "你是一位资深的敏感词校验工程师,请你校验用户输入的文本内容是否包含敏感词汇,并将命中的结果以json格式输出,示例如下:{\"checkPass\": false, \"content\":\"fuck\"}" + + "字段说明:checkPass布尔值,包含敏感词为false、不包含敏感词为true。content为字符类型,里面填充的是你检测到的所有的敏感词"; + Long startTime = System.currentTimeMillis(); + JSONObject requestBody = new JSONObject(); + requestBody.put("model", "ep-20250917134025-t2hfr"); + JSONArray messages = new JSONArray(); + JSONObject systemMessage = new JSONObject(); + systemMessage.put("role", ChatMessageRole.SYSTEM.value()); + systemMessage.put("content", systemPrompt); + messages.add(systemMessage); + JSONObject userMessage = new JSONObject(); + userMessage.put("role", ChatMessageRole.USER.value()); + userMessage.put("content", "今天天气好哟,我日你,我草死你,你个婊子,奥巴马"); + messages.add(userMessage); + requestBody.put("messages", messages); + JSONObject thinking = new JSONObject(); + thinking.put("type", "disabled"); + requestBody.put("thinking", thinking); + log.info("===> aiGen body : {}", requestBody); + HttpResponse response = Unirest.post("https://ark.ap-southeast.bytepluses.com/api/v3/chat/completions") + .header("Authorization", "Bearer " + "4075846b-cb9e-45f6-bf59-ab0a94745d9b") + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .asString(); + String result = response.getBody(); + log.info("===> aiGen result : {}", result); + JSONObject jsonObject = JSONObject.parseObject(result); + String content = jsonObject.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content"); + Long endTime = System.currentTimeMillis(); + log.info("===> aiGen time : {}", (endTime - startTime) / 1000); + System.out.println(content); + } catch (UnirestException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/DeepSeekClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/DeepSeekClientImpl.java new file mode 100644 index 0000000..e690253 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/DeepSeekClientImpl.java @@ -0,0 +1,45 @@ +package com.sonic.cow.client.impl; + +import com.byteplus.ark.runtime.model.completion.chat.ChatCompletionRequest; +import com.byteplus.ark.runtime.model.completion.chat.ChatCompletionResult; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessage; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import com.byteplus.ark.runtime.service.ArkService; +import com.sonic.cow.client.DeepSeekClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +@Slf4j +@Service +public class DeepSeekClientImpl implements DeepSeekClient { + + @Value("${volcengine.deepSeekModel}") + private String contextCachingModel; + + @Autowired + private ArkService volcengineArkService; + + @Override + public String aiGen(String systemPrompt, Double temperature) { + log.info("===>DeepSeekClientImpl aiGen systemPrompt : {}", systemPrompt); + Long startTime = System.currentTimeMillis(); + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(contextCachingModel) + .messages(Collections.singletonList(ChatMessage.builder() + .role(ChatMessageRole.SYSTEM) + .content(systemPrompt).build())) + .temperature(temperature == null ? 0.5 : temperature) + .build(); + ChatCompletionResult chatCompletionResult = volcengineArkService.createChatCompletion(chatCompletionRequest); + StringBuffer stringBuffer = new StringBuffer(); + chatCompletionResult.getChoices().forEach(choice -> stringBuffer.append(choice.getMessage().getContent())); + Long endTime = System.currentTimeMillis(); + log.info("===>DeepSeekClientImpl aiGen time : {}", (endTime - startTime) / 1000); + return stringBuffer.toString(); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ImageToImageClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ImageToImageClientImpl.java new file mode 100644 index 0000000..8d05abf --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ImageToImageClientImpl.java @@ -0,0 +1,184 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Lists; +import com.sonic.cow.client.ImageGenImageClient; +import com.sonic.cow.utils.LimitUtils; +import com.sonic.cow.utils.MD5Util; +import com.sonic.cow.utils.RedisKeyUtils; +import com.volcengine.service.visual.IVisualService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 文档地址:https://www.volcengine.com/docs/85128/1584306#YLSQPqOs + * SDK说明:https://www.volcengine.com/docs/6444/1340578 + * 实例:https://github.com/volcengine/volc-sdk-java/blob/main/example/src/main/java/com/volcengine/example/visual/CVProcessDemo.java + */ +@Slf4j +@Service +public class ImageToImageClientImpl implements ImageGenImageClient { + + @Autowired + private IVisualService visualService; + @Autowired + private LimitUtils limitUtils; + @Autowired + private RedisKeyUtils redisKeyUtils; + + /** + * 可重试的code + */ + private static final List RETRIED_CODE = Lists.newArrayList("50511", "50429", "50430", "50500", "50501"); + + + @Override + public String imageGenImage(String imageUrl, String prompt) throws Exception { + try { + Long startTime = System.currentTimeMillis(); + + JSONObject req = new JSONObject(); + //下面三个必填参数 + req.put("req_key", "i2i_portrait_photo"); + req.put("image_input", imageUrl); + req.put("prompt", prompt); + + //下面参数非必填参数,可自行选择是否添加 + req.put("gpen", "0.4"); + req.put("skin", "0.3"); + req.put("skin_unifi", 0); + //图片比例 + req.put("width", "720"); + req.put("height", "1280"); + req.put("gen_mode", "creative"); + req.put("seed", "-1"); + req.put("return_url", false); + + Object response = visualService.cvProcess(req); + JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(response)); + log.info("imageToImage 图生成图片结果:{}", jsonObject); + Long endTime = System.currentTimeMillis(); + log.info("imageToImage 图生成图片所需要的时间:{}", (endTime - startTime) / 1000); + try { + //睡眠个3秒,等待资源释放,下一个请求才不会报并发限制 + Thread.sleep(3000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return jsonObject.getJSONObject("data").getJSONArray("binary_data_base64").get(0).toString(); + } catch (Exception e) { + log.error("imageGenImage error:", e); +// String message = e.getMessage(); +// JSONObject errorObject = JSONObject.parseObject(message); +// String code = errorObject.getString("code"); + String md5Prompt = MD5Util.digest(prompt); + String imageGenImageErrorEnumKey = redisKeyUtils.imageGenImageErrorEnumKey(md5Prompt); + int errorNum = limitUtils.defaultLimitCheckReturnCount(imageGenImageErrorEnumKey, 10 * 60); + log.info("imageGenImage errorNum:{}", errorNum); + //最多重试5次 + if (errorNum <= 5) { + log.info("imageGenImage 重试次数:{}", errorNum); + //调用图生图 角色特征保持方法 + return imageGenImageV2(imageUrl, prompt); + } + throw new Exception(e.getMessage()); + } + } + + @Override + public String imageGenImageV2(String imageUrl, String prompt) throws Exception { + try { + Long startTime = System.currentTimeMillis(); + + JSONObject req = new JSONObject(); + //请求Body(查看接口文档请求参数-请求示例,将请求参数内容复制到此) + req.put("req_key", "seed3l_single_ip"); + req.put("image_urls", new String[]{imageUrl}); + req.put("prompt", prompt); + req.put("seed", "-1"); + req.put("width", "720"); + req.put("height", "1280"); + req.put("gen_mode", "creative"); + req.put("return_url", false); + + Object response = visualService.cvProcess(req); + JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(response)); + log.info("imageToImage 图生成图片结果:{}", jsonObject); + Long endTime = System.currentTimeMillis(); + log.info("imageGenImageV2 图生成图片所需要的时间:{}", (endTime - startTime) / 1000); + try { + //睡眠个3秒,等待资源释放,下一个请求才不会报并发限制 + Thread.sleep(3000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return jsonObject.getJSONObject("data").getJSONArray("binary_data_base64").get(0).toString(); + } catch (Exception e) { + log.error("imageGenImageV2 error:", e); +// String message = e.getMessage(); +// JSONObject errorObject = JSONObject.parseObject(message); +// String code = errorObject.getString("code"); + String md5Prompt = MD5Util.digest(prompt); + String imageGenImageErrorEnumKey = redisKeyUtils.imageGenImageErrorEnumKey(md5Prompt); + int errorNum = limitUtils.defaultLimitCheckReturnCount(imageGenImageErrorEnumKey, 10 * 60); + log.info("imageGenImageV2 errorNum:{}", errorNum); + //最多重试5次 + if (errorNum <= 5) { + log.info("imageGenImageV2 重试次数:{}", errorNum); + //调用图生图 人物写真方法 + return imageGenImage(imageUrl, prompt); + } + throw new Exception(e.getMessage()); + } + } + + @Override + public String imageGenImageSubmitTask(String imageUrl, String prompt) { + JSONObject req = new JSONObject(); + //请求Body(查看接口文档请求参数-请求示例,将请求参数内容复制到此) + req.put("req_key", "i2i_portrait_photo"); + req.put("image_input", imageUrl); + req.put("prompt", prompt); + req.put("width", "720"); + req.put("height", "1280"); + try { + Long startTime = System.currentTimeMillis(); + Object response = visualService.cvSync2AsyncSubmitTask(req); + JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(response)); + Long endTime = System.currentTimeMillis(); + log.info("imageToImage 图生成图片提交任务结果所需要的时间:{}", (endTime - startTime) / 1000); + return jsonObject.getJSONObject("data").getString("task_id"); + } catch (Exception e) { + log.error("imageGenImageSubmitTask error:", e); + return ""; + } + } + + @Override + public String imageGenImageGetTaskResult(String taskId) { + JSONObject req = new JSONObject(); + //请求Body(查看接口文档请求参数-请求示例,将请求参数内容复制到此) + req.put("req_key", "i2i_portrait_photo"); + req.put("task_id", "14366847939889876549"); +// req.put("req_json",""); + try { + Long startTime = System.currentTimeMillis(); + + Object response = visualService.cvSync2AsyncGetResult(req); + JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(response)); + + Long endTime = System.currentTimeMillis(); + log.info("imageToImage 图生成图片获取任务结果所需要的时间:{}", (endTime - startTime) / 1000); + return jsonObject.getJSONObject("data").getJSONArray("binary_data_base64").get(0).toString(); + } catch (Exception e) { + log.error("imageGenImageSubmitTask error:", e); + return ""; + } + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ResponseChatClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ResponseChatClientImpl.java new file mode 100644 index 0000000..ae5df28 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/ResponseChatClientImpl.java @@ -0,0 +1,210 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import com.sonic.cow.client.ResponseChatClient; +import com.sonic.cow.client.request.*; +import com.sonic.cow.client.response.ResponseChatResponse; +import com.sonic.cow.domain.bo.ChatOutputTextBo; +import com.sonic.cow.utils.JsonExtractor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class ResponseChatClientImpl implements ResponseChatClient { + + @Value("${volcengine.apiKey}") + private String apiKey; + @Value("${volcengine.contextCachingModel}") + private String contextCachingModel; + @Value("${volcengine.responseUrl:https://ark.ap-southeast.bytepluses.com/api/v3/responses}") + private String responseUrl; + + @Override + public ResponseChatResponse responseStructuredChat(String responseId, String instructions, List inputRequests, List tools, String functionParam, Float temperature, Float topp) { + ResponseChatResponse response = responseChatV2(responseId, instructions, inputRequests, tools, null, true, functionParam, temperature, topp); + if(response == null) { + return null; + } + //处理格式化输出 + ChatOutputTextBo bo = JsonExtractor.extractMessageAndScore(response.getMessage()); + if(bo != null) { + response.setMessage(bo.getMessage()); + response.setScore(bo.getScore()); + } + return response; + } + + @Override + public ResponseChatResponse responseStructuredChat(String responseId, String instructions, List inputRequests, List tools, boolean caching) { + ResponseChatResponse response = responseChat(responseId, instructions, inputRequests, tools, null, caching); + if(response == null) { + return null; + } + //处理格式化输出 + ChatOutputTextBo bo = JsonExtractor.extractMessageAndScore(response.getMessage()); + if(bo != null) { + response.setMessage(bo.getMessage()); + response.setScore(bo.getScore()); + } + return response; + } + + + @Override + public ResponseChatResponse responseChat(String responseId, String instructions, List inputRequests, List tools, Text text, boolean caching) { + try { + ResponseChatCompletionRequest request = ResponseChatCompletionRequest.builder() + .model(contextCachingModel) + .previousResponseId(responseId) + .instructions(instructions) + .input(inputRequests) + .text(text) + .thinking(Thinking.builder().type("disabled").build()) + .caching(Caching.builder().type(caching ? "enabled" : "disabled").build()) + .store(true) + .tools(StringUtils.isEmpty(responseId) ? tools : null) + .build(); + ObjectMapper mapper = new ObjectMapper(); + String requestBody = mapper.writeValueAsString(request); + log.info("===> responseChat requestBody : {}", requestBody); + HttpResponse response = Unirest.post(responseUrl) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .body(requestBody) + .asString(); + String body = response.getBody(); + log.info("===> responseChat responseBody : {}", body); + //解析出id、内容 + JSONObject jsonObject = JSONObject.parseObject(body); + JSONObject error = jsonObject.getJSONObject("error"); + if(error != null && StringUtils.isNotEmpty(error.getString("code"))) { + //返回空,并删除缓存 + return ResponseChatResponse.builder() + .code(error.getString("code")) + .build(); + } + String respId = jsonObject.getString("id"); + JSONObject outputJSONObject = jsonObject.getJSONArray("output").getJSONObject(0); + JSONObject contentJSONObject = outputJSONObject.getJSONArray("content") == null ? null : outputJSONObject.getJSONArray("content").getJSONObject(0); + String content = contentJSONObject == null ? null : contentJSONObject.getString("text"); + String type = outputJSONObject.getString("type"); + String name = outputJSONObject.getString("name"); + String arguments = outputJSONObject.getString("arguments"); + String callId = outputJSONObject.getString("call_id"); + //构造出参对象 + return ResponseChatResponse.builder() + .responseId(respId) + .message(content) + .type(type) + .functionCallName(name) + .functionCallArguments(arguments) + .functionCallId(callId) + .build(); + } catch (Exception e) { + log.error("===> responseChat error : ", e); + } + return null; + } + + @Override + public ResponseChatResponse responseChatV2(String responseId, String instructions, List inputRequests, List tools, Text text, boolean caching, String functionParam, Float temperature, Float topp) { + try { + ResponseChatCompletionRequest request = ResponseChatCompletionRequest.builder() + .model(contextCachingModel) + .previousResponseId(responseId) + .instructions(instructions) + .input(inputRequests) + .text(text) + .thinking(Thinking.builder().type("disabled").build()) + .caching(Caching.builder().type(caching ? "enabled" : "disabled").build()) + .store(true) + .tools(StringUtils.isEmpty(responseId) ? tools : null) + .temperature(temperature == null ? 1.0f : temperature) + .topP(topp == null ? 0.7f : topp) + .build(); + ObjectMapper mapper = new ObjectMapper(); + String requestBody = mapper.writeValueAsString(request); + log.info("===> responseChatV2 requestBody : {}", requestBody); + HttpResponse response = Unirest.post(responseUrl) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .body(requestBody) + .asString(); + String body = response.getBody(); + log.info("===> responseChatV2 responseBody : {}", body); + //解析出id、内容 + JSONObject jsonObject = JSONObject.parseObject(body); + JSONObject error = jsonObject.getJSONObject("error"); + if(error != null && StringUtils.isNotEmpty(error.getString("code"))) { + //返回空,并删除缓存 + return ResponseChatResponse.builder() + .code(error.getString("code")) + .build(); + } + String respId = jsonObject.getString("id"); + JSONArray outputList = jsonObject.getJSONArray("output"); + for (int i = 0; i < outputList.size(); i++) { + JSONObject outputJSONObject = outputList.getJSONObject(i); + JSONObject contentJSONObject = outputJSONObject.getJSONArray("content") == null ? null : outputJSONObject.getJSONArray("content").getJSONObject(0); + String content = contentJSONObject == null ? null : contentJSONObject.getString("text"); + String type = outputJSONObject.getString("type"); + String name = outputJSONObject.getString("name"); + String arguments = outputJSONObject.getString("arguments"); + String callId = outputJSONObject.getString("call_id"); + //判断文本内容是否有值,如果有值的话那么直接快速返回 + if(StringUtils.isNotEmpty(content)) { + //判断内容中是否有函数调用的方法名称,为了兼容返回混乱json结果的问题 + if(content.indexOf("get_ai_image") > 0) { + return ResponseChatResponse.builder() + .responseId(respId) + .message(content) + .type("function_call") + .functionCallName("get_ai_image") + .functionCallArguments(functionParam) + .functionCallId(callId) + .build(); + } + ChatOutputTextBo bo = JsonExtractor.extractMessageAndScore(content); + if(StringUtils.isNotEmpty(bo.getMessage()) && !bo.getMessage().equals(content)) { + //构造出参对象,直接快速返回掉 + return ResponseChatResponse.builder() + .responseId(respId) + .message(content) + .type(type) + .functionCallName(name) + .functionCallArguments(arguments) + .functionCallId(callId) + .build(); + } + //当出现多条数据,第一条为文本,第二条为function call,那么文本值为空的时候跳出本次循环,执行下一条数据的逻辑 + //并且还有下一条数据的时候才跳出循环 + if(StringUtils.isEmpty(bo.getMessage()) && i < outputList.size()) { + continue; + } + } + //构造出参对象 + return ResponseChatResponse.builder() + .responseId(respId) + .message(content) + .type(type) + .functionCallName(name) + .functionCallArguments(arguments) + .functionCallId(callId) + .build(); + } + } catch (Exception e) { + log.error("===> responseChatV2 error : ", e); + } + return null; + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/Seedream4ImageClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/Seedream4ImageClientImpl.java new file mode 100644 index 0000000..0ecd315 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/Seedream4ImageClientImpl.java @@ -0,0 +1,148 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.byteplus.ark.runtime.model.images.generation.GenerateImagesRequest; +import com.byteplus.ark.runtime.model.images.generation.ImagesResponse; +import com.byteplus.ark.runtime.model.images.generation.ResponseFormat; +import com.byteplus.ark.runtime.service.ArkService; +import com.google.common.collect.Maps; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.JsonNode; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.client.Seedream4GenImageClient; +import com.sonic.cow.client.TextGenImageClient; +import com.sonic.cow.utils.LimitUtils; +import com.sonic.cow.utils.MD5Util; +import com.sonic.cow.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +public class Seedream4ImageClientImpl implements Seedream4GenImageClient { + + @Value("${volcengine.seedream4Model}") + private String seedream4Model; + @Value("${volcengine.apiKey}") + private String apiKey; + + + @Autowired + private ArkService volcengineArkService; + @Autowired + private LimitUtils limitUtils; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @Override + public String genImage(String imageUrl, String prompt) throws Exception { + try { + // 构建请求体 JSON + Map requestBody = Maps.newHashMap(); + requestBody.put("model", seedream4Model); + requestBody.put("prompt", prompt); + if (StringUtils.isNotBlank(imageUrl)) { + requestBody.put("image", imageUrl); + } + requestBody.put("sequential_image_generation", "disabled"); + requestBody.put("response_format", "b64_json"); + requestBody.put("size", "1440x2560"); + requestBody.put("stream", false); + requestBody.put("watermark", false); + + // 发送 POST 请求 + HttpResponse response = Unirest.post("https://ark.ap-southeast.bytepluses.com/api/v3/images/generations") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) // 拼接 Bearer 令牌 + .body(JSON.toJSONString(requestBody)) // 设置请求体 + .asJson(); // 以 JSON 格式接收响应 + // 处理响应 + if (response.getStatus() == 200) { + log.info("请求成功,响应结果:{}", response.getBody().toString()); + // 可从 response.getBody() 中解析生成的图片 URL 等信息 + String base64 = response.getBody().getObject().getJSONArray("data").getJSONObject(0).getString("b64_json"); + return base64; + } else { + log.info("请求失败,状态码::{}", response.getStatus()); + log.info("错误信息::{}", response.getBody()); + throw new Exception(JSON.toJSONString(response.getBody())); + } + } catch (Exception e) { + log.error("===> seedream4 genImage error: ", e); + String md5Prompt = MD5Util.digest(prompt); + String imageGenImageErrorEnumKey = redisKeyUtils.imageGenImageErrorEnumKey(md5Prompt); + int errorNum = limitUtils.defaultLimitCheckReturnCount(imageGenImageErrorEnumKey, 10 * 60); + log.info("seedream4 imageGenImage errorNum:{}", errorNum); + //最多重试2次 + if (errorNum <= 2) { + log.info("seedream4 imageGenImage 重试次数:{}", errorNum); + //调用图生图 角色特征保持方法 + return genImage(imageUrl, prompt); + } + throw new Exception(e.getMessage()); + } finally { + volcengineArkService.shutdownExecutor(); + } + } + + public static void main(String[] args) { + // 替换为你的 ARK API 密钥 + String arkApiKey = "4075846b-cb9e-45f6-bf59-ab0a94745d9b"; + + try { + // 构建请求体 JSON +// String requestBody = "{" + +// "\"model\": \"ep-20250911184318-89dxw\"," + +// "\"prompt\": \"海滩,穿着比基尼,悠闲散步,手里拿着椰子,张开双手,享受海风,开怀大笑\"," + +// "\"image\": \"https://hhb.crushlevel.ai/dev/role/17562030935666537.jpg\"," + +// "\"sequential_image_generation\": \"disabled\"," + +//// "\"response_format\": \"b64_json\"," + +// "\"response_format\": \"url\"," + +// "\"size\": \"2K\"," + +// "\"stream\": false," + +// "\"watermark\": true" + +// "}"; + + Map requestBody = Maps.newHashMap(); + requestBody.put("model", "ep-20250911184318-89dxw"); + requestBody.put("prompt", "海滩,穿着比基尼,悠闲散步,手里拿着椰子,张开双手,享受海风,开怀大笑"); +// requestBody.put("image", "https://hhb.crushlevel.ai/dev/role/17562030935666537.jpg"); + requestBody.put("sequential_image_generation", "disabled"); + requestBody.put("response_format", "url"); +// requestBody.put("response_format", "b64_json"); + requestBody.put("size", "1440x2560"); + requestBody.put("stream", false); + requestBody.put("watermark", false); + + // 发送 POST 请求 + HttpResponse response = Unirest.post("https://ark.ap-southeast.bytepluses.com/api/v3/images/generations") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + arkApiKey) // 拼接 Bearer 令牌 + .body(JSON.toJSONString(requestBody)) // 设置请求体 + .asJson(); // 以 JSON 格式接收响应 + // 处理响应 + if (response.getStatus() == 200) { + System.out.println("请求成功,响应结果:"); + System.out.println(response.getBody().toString()); + // 可从 response.getBody() 中解析生成的图片 URL 等信息 + System.out.println("图片 URL:" + response.getBody().getObject().getJSONArray("data").getJSONObject(0).getString("url")); + } else { + System.out.println("请求失败,状态码:" + response.getStatus()); + System.out.println("错误信息:" + response.getBody()); + } + + } catch (Exception e) { + e.printStackTrace(); + } finally { + + } + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TextToImageClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TextToImageClientImpl.java new file mode 100644 index 0000000..31fd13c --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TextToImageClientImpl.java @@ -0,0 +1,50 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSON; +import com.sonic.cow.client.TextGenImageClient; +import com.byteplus.ark.runtime.model.images.generation.GenerateImagesRequest; +import com.byteplus.ark.runtime.model.images.generation.ImagesResponse; +import com.byteplus.ark.runtime.model.images.generation.ResponseFormat; +import com.byteplus.ark.runtime.service.ArkService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class TextToImageClientImpl implements TextGenImageClient { + + @Value("${volcengine.textToImageModel}") + private String textToImageModel; + + @Autowired + private ArkService volcengineArkService; + + @Override + public String textGenImage(String prompt) throws Exception{ + try { + Long startTime = System.currentTimeMillis(); + GenerateImagesRequest generateRequest = GenerateImagesRequest.builder() + .model(textToImageModel) + .prompt(prompt) + .responseFormat(ResponseFormat.Base64) + .seed(1234567890) + .watermark(false) + //图片大小待确定 + .size("720x1280") + .guidanceScale(2.5) + .build(); + ImagesResponse imagesResponse = volcengineArkService.generateImages(generateRequest); + log.info("textToImage imagesResponse:" + JSON.toJSONString(imagesResponse)); + Long endTime = System.currentTimeMillis(); + log.info("textToImage 生成图片所需要的时间:{}", (endTime - startTime) / 1000); + return imagesResponse.getData().get(0).getB64Json(); + } catch (Exception e) { + log.error("textToImage error:", e); + throw new Exception(e.getMessage()); + } finally { + volcengineArkService.shutdownExecutor(); + } + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TtsClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TtsClientImpl.java new file mode 100644 index 0000000..3cd7d85 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TtsClientImpl.java @@ -0,0 +1,60 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSONObject; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.client.TtsClient; +import com.sonic.cow.client.config.TtsCnConfig; +import com.sonic.cow.client.input.tts.TtsInput; +import com.sonic.cow.client.output.tts.TtsOutput; +import com.sonic.cow.domain.input.VoiceTtsInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class TtsClientImpl implements TtsClient { + + @Autowired + private TtsCnConfig ttsCnConfig; + + @Override + public String tts(String userId, VoiceTtsInput input) { + //构造入参对象 + TtsInput ttsInput = new TtsInput(ttsCnConfig.getTtsAppId(), ttsCnConfig.getCluster(), input.getVoiceType()); + + //设置用户ID + ttsInput.getUser().setUid(userId); + //设置音频编码格式 + ttsInput.getAudio().setEncoding("wav"); + + //设置语速 + ttsInput.getAudio().setLoudness_ratio(input.getLoudnessRatio()); + ttsInput.getAudio().setSpeed_ratio(input.getSpeedRatio()); + + //设置入参文本内容 + ttsInput.getRequest().setText(input.getText()); + log.info("===> asr input : {}", JSONObject.toJSONString(ttsInput)); + try { + //RPC 生成语音 + HttpResponse result = Unirest.post("https://openspeech.bytedance.com/api/v1/tts") + .header("Authorization", "Bearer; " + ttsCnConfig.getTtsAccessToken()) + .body(JSONObject.toJSONString(ttsInput)) + .asString(); + String body = result.getBody(); + log.info("===> asr body : {}", body); + TtsOutput response = JSONObject.parseObject(body, TtsOutput.class); + //获取data数据并存储到文件中 + System.out.println("==========> start"); + System.out.println(response.getData()); + System.out.println("==========> end"); + return response.getData(); + } catch (UnirestException e) { + log.error("===> asr error : ", e); + } + return null; + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TtsV2ClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TtsV2ClientImpl.java new file mode 100644 index 0000000..a0d05f1 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TtsV2ClientImpl.java @@ -0,0 +1,138 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSON; +import com.mashape.unirest.http.Headers; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.client.TtsV2Client; +import com.sonic.cow.client.config.TtsConfig; +import com.sonic.cow.domain.input.VoiceTtsV2Input; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Base64; +import java.util.Set; + +@Slf4j +@Service +public class TtsV2ClientImpl implements TtsV2Client { + + @Autowired + private TtsConfig ttsConfig; + /** + * 资源ID + */ + private static final String resourceId = "volc.megatts.default"; + /** + * 请求地址 + */ + private static final String url = "https://voice.ap-southeast-1.bytepluses.com/api/v3/tts/unidirectional"; + + @Override + public String tts(String userId, VoiceTtsV2Input input) { + HttpResponse response; + try { + // additions + JSONObject additions = new JSONObject(); + additions.put("disable_markdown_filter", true); + additions.put("enable_language_detector", true); + additions.put("enable_latex_tn", true); + additions.put("disable_default_bit_rate", true); + additions.put("max_length_to_filter_parenthesis", 0); + JSONObject cacheConfig = new JSONObject(); + cacheConfig.put("text_type", 1); + cacheConfig.put("use_cache", false); + additions.put("cache_config", cacheConfig); + String additionsJson = additions.toString(); + + // payload + JSONObject payload = new JSONObject(); + JSONObject user = new JSONObject(); + user.put("uid", userId); + payload.put("user", user); + JSONObject reqParams = new JSONObject(); + reqParams.put("text", input.getText()); + reqParams.put("speaker", input.getVoiceType()); + reqParams.put("additions", additionsJson); + JSONObject audioParams = new JSONObject(); + audioParams.put("format", "mp3"); + audioParams.put("sample_rate", 24000); + //设置语速 + audioParams.put("speech_rate", input.getSpeechRate()); + + //设置音高 + JSONObject postProcess = new JSONObject(); + postProcess.put("pitch", input.getPitchRate()); + audioParams.put("post_process", postProcess); + + reqParams.put("audio_params", audioParams); + payload.put("req_params", reqParams); + + log.info("===> tts param : {}", payload); + + // request + response = Unirest.post(url) + .header("X-Api-App-Id", ttsConfig.getTtsAppId()) + .header("X-Api-Access-Key", ttsConfig.getTtsAccessToken()) + .header("X-Api-Resource-Id", resourceId) + .header("X-Api-App-Key", "aGjiRDfUWi") + .header("Content-Type", "application/json") + .header("Connection", "keep-alive") + .body(payload.toString()) + .asBinary(); + Headers headers = response.getHeaders(); + log.info("===> tts response headers: {}", JSON.toJSONString(headers)); + // process stream + try (InputStream is = response.getBody(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { + + ByteArrayOutputStream audioData = new ByteArrayOutputStream(); + int totalAudioSize = 0; + String line; + while ((line = reader.readLine()) != null) { + if (line.isEmpty()) { + continue; + } + JSONObject data = new JSONObject(line); + log.info("json data:{}", JSON.toJSONString(data)); + + int code = data.optInt("code", 0); + if (code == 0) { + if (data.has("data") && !data.isNull("data") && !data.getString("data").isEmpty()) { + byte[] chunkAudio = Base64.getDecoder().decode(data.getString("data")); + int audioSize = chunkAudio.length; + totalAudioSize += audioSize; + audioData.write(chunkAudio); + } + } else if (code == 20000000) { + break; + } else if (code > 0) { + log.info("error response:" + data); + break; + } + } + + // save audio data to local file + if (audioData.size() > 0) { + + String audioDataBase64 = Base64.getEncoder().encodeToString(audioData.toByteArray()); + log.info("=================================="); + log.info(audioDataBase64); + log.info("=================================="); + return audioDataBase64; + } + } + } catch (UnirestException | IOException e) { + log.error("===> request error: ", e); + } + return null; + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TtsV3ClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TtsV3ClientImpl.java new file mode 100644 index 0000000..5370af3 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/TtsV3ClientImpl.java @@ -0,0 +1,204 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSON; +import com.mashape.unirest.http.Headers; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.client.TtsV2Client; +import com.sonic.cow.client.TtsV3Client; +import com.sonic.cow.client.config.TtsConfig; +import com.sonic.cow.domain.input.VoiceTtsV2Input; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.Header; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.config.SocketConfig; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.pool.PoolStats; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.util.Base64; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class TtsV3ClientImpl implements TtsV3Client { + + @Autowired + private TtsConfig ttsConfig; + /** + * 资源ID + */ + private static final String resourceId = "volc.megatts.default"; + /** + * 请求地址 + */ + private static final String url = "https://voice.ap-southeast-1.bytepluses.com/api/v3/tts/unidirectional"; + + // 静态初始化:连接池和客户端 + private static final PoolingHttpClientConnectionManager connectionManager; + private static final CloseableHttpClient httpClient; + + static { + // 连接池配置 + connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(10); + connectionManager.setDefaultMaxPerRoute(10); + + // Socket 配置:读写超时 + SocketConfig socketConfig = SocketConfig.custom() + .setSoTimeout(30000) // 读超时 30s(TTS 流式响应) + .build(); + connectionManager.setDefaultSocketConfig(socketConfig); + + // 请求配置:连接请求超时 + 其他超时 + RequestConfig defaultConfig = RequestConfig.custom() + .setConnectionRequestTimeout(5000) // 从池获取连接超时 5s + .setConnectTimeout(5000) // 建立新连接超时 5s + .setSocketTimeout(30000) // 读超时 30s + .build(); + + // 构建客户端,应用默认配置 + httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(defaultConfig) + .setConnectionTimeToLive(30, TimeUnit.SECONDS) // 连接最大存活 30s,过期自动清理 + .build(); + } + + @Override + public String tts(String userId, VoiceTtsV2Input input) { + CloseableHttpResponse response = null; + try { + // additions + JSONObject additions = new JSONObject(); + additions.put("disable_markdown_filter", true); + additions.put("enable_language_detector", true); + additions.put("enable_latex_tn", true); + additions.put("disable_default_bit_rate", true); + additions.put("max_length_to_filter_parenthesis", 0); + JSONObject cacheConfig = new JSONObject(); + cacheConfig.put("text_type", 1); + cacheConfig.put("use_cache", false); + additions.put("cache_config", cacheConfig); + String additionsJson = additions.toString(); + + // payload + JSONObject payload = new JSONObject(); + JSONObject user = new JSONObject(); + user.put("uid", userId); + payload.put("user", user); + JSONObject reqParams = new JSONObject(); + reqParams.put("text", input.getText()); + reqParams.put("speaker", input.getVoiceType()); + reqParams.put("additions", additionsJson); + JSONObject audioParams = new JSONObject(); + audioParams.put("format", "mp3"); + audioParams.put("sample_rate", 24000); + //设置语速 + audioParams.put("speech_rate", input.getSpeechRate()); + + //设置音高 + JSONObject postProcess = new JSONObject(); + postProcess.put("pitch", input.getPitchRate()); + audioParams.put("post_process", postProcess); + + reqParams.put("audio_params", audioParams); + payload.put("req_params", reqParams); + + log.info("===> tts param : {}", payload); + + //获取连接状态 + PoolStats totalStats = connectionManager.getTotalStats(); + int totalConnections = connectionManager.getMaxTotal(); // 总数(上限) + int leasedConnections = totalStats.getLeased(); // 已使用 + int availableConnections = totalStats.getAvailable(); // 空闲 + int pendingConnections = totalStats.getPending(); // 等待中 + int createdTotal = leasedConnections + availableConnections; // 当前已创建总数 + log.info("===> http pool 总数上限: {}, 已创建: {}, 已使用: {}, 空闲: {}, 等待: {}", + totalConnections, createdTotal, leasedConnections, availableConnections, pendingConnections); + + // 构建 POST 请求 + HttpPost httpPost = new HttpPost(url); + httpPost.setHeader("X-Api-App-Id", ttsConfig.getTtsAppId()); + httpPost.setHeader("X-Api-Access-Key", ttsConfig.getTtsAccessToken()); + httpPost.setHeader("X-Api-Resource-Id", resourceId); + httpPost.setHeader("X-Api-App-Key", "aGjiRDfUWi"); + httpPost.setHeader("Content-Type", "application/json"); + httpPost.setHeader("Connection", "close"); + + StringEntity entity = new StringEntity(payload.toString(), "UTF-8"); + httpPost.setEntity(entity); + + // 执行请求 + response = httpClient.execute(httpPost); + + // 日志响应 headers(类似原代码) + Header[] headers = response.getAllHeaders(); + log.info("===> tts response headers: {}", JSON.toJSONString(headers)); // 需调整为数组格式,如果 JSON 需要 + + // 处理流式响应 + try (InputStream is = response.getEntity().getContent(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { + + ByteArrayOutputStream audioData = new ByteArrayOutputStream(); + int totalAudioSize = 0; + String line; + boolean endOfStream = false; + while ((line = reader.readLine()) != null) { + if (line.isEmpty()) continue; + JSONObject data = new JSONObject(line); + log.info("json data:{}", JSON.toJSONString(data)); + + int code = data.optInt("code", 0); + if (code == 0) { + if (data.has("data") && !data.isNull("data") && !data.getString("data").isEmpty()) { + byte[] chunkAudio = Base64.getDecoder().decode(data.getString("data")); + totalAudioSize += chunkAudio.length; + audioData.write(chunkAudio); + } + } else if (code == 20000000) { + endOfStream = true; // 流结束信号 + break; + } else if (code > 0) { + log.info("error response:" + data); + break; + } + } + if (!endOfStream) { + log.warn("Stream may not be fully consumed, forcing EOF check"); + } + + if (audioData.size() > 0) { + String audioDataBase64 = Base64.getEncoder().encodeToString(audioData.toByteArray()); + log.info("=================================="); + log.info(audioDataBase64); + log.info("=================================="); + return audioDataBase64; + } + } + + } catch (IOException e) { // HttpClient 异常主要是 IOException + log.error("===> request error: ", e); + } finally { + // 关闭响应(自动释放 entity) + if (response != null) { + try { + response.close(); + } catch (IOException e) { + log.error("Error closing response", e); + } + } + } + return null; + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/VoiceChatClientImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/VoiceChatClientImpl.java new file mode 100644 index 0000000..7949aa3 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/impl/VoiceChatClientImpl.java @@ -0,0 +1,305 @@ +package com.sonic.cow.client.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Maps; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.client.VoiceChatClient; +import com.sonic.cow.client.input.voicechat.StartVoiceChatInput; +import com.sonic.cow.client.input.voicechat.StopVoiceChatInput; +import com.sonic.cow.client.input.voicechat.UpdateVoiceChatInput; +import com.sonic.cow.client.input.voicechat.domain.*; +import com.sonic.cow.client.output.voicechat.VoiceChatResponse; +import com.sonic.cow.client.rtc.*; +import com.sonic.cow.client.rtc.callback.conversation.Conv; +import com.sonic.cow.client.rtc.callback.RtsMessage; +import com.sonic.cow.client.rtc.callback.conversation.ConversationMessageParse; +import com.sonic.cow.client.rtc.callback.rts.MessageParse; +import com.sonic.cow.client.rtc.callback.rts.Subv; +import com.sonic.cow.domain.input.VoiceChatOptInput; +import com.sonic.cow.service.CommonSendMqService; +import com.sonic.cow.utils.HttpUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class VoiceChatClientImpl implements VoiceChatClient { + + private final StringRedisTemplate stringRedisTemplate; + @Value("${volcengine.rtc.appId}") + private String rtcAppId; + @Value("${volcengine.rtc.appKey}") + private String rtcAppKey; + @Value("${volcengine.rtc.url}") + private String rtcUrl; + @Value("${volcengine.rtc.action.startVoiceChat}") + private String startVoiceChatAction; + @Value("${volcengine.rtc.action.stopVoiceChat}") + private String stopVoiceChatAction; + @Value("${volcengine.rtc.action.updateVoiceChat}") + private String updateVoiceChatAction; + @Value("${volcengine.rtc.version}") + private String rtcVersion; + + @Value("${volcengine.accessKey}") + private String accessKey; + @Value("${volcengine.secretKey}") + private String secretKey; + @Value("${volcengine.region}") + private String region; + @Value("${volcengine.rtc.host}") + private String rtcHost; + @Value("${volcengine.rtc.service}") + private String service; + @Value("${volcengine.asr.appId}") + private String asrAppId; + @Value("${volcengine.asr.accessToken}") + private String asrAccessToken; + @Value("${volcengine.asr.provider}") + private String asrProvider; + @Value("${volcengine.tts.appId}") + private String ttsAppId; + @Value("${volcengine.tts.accessToken}") + private String ttsAccessToken; + @Value("${volcengine.tts.provider}") + private String ttsProvider; + @Value("${volcengine.llm.endPointId}") + private String llmEndPointId; + @Value("${volcengine.llm.apiKey}") + private String llmApiKey; + @Value("${volcengine.rtc.webhookSecret}") + private String webhookSecret; + + @Value("${volcengine.rtc.subtitleServiceUrl}") + private String subtitleServiceUrl; + @Value("${volcengine.rtc.subtitleServiceSignature}") + private String subtitleServiceSignature; + + + @Autowired + private CommonSendMqService commonSendMqService; + + public VoiceChatClientImpl(StringRedisTemplate stringRedisTemplate) { + this.stringRedisTemplate = stringRedisTemplate; + } + + /** + * 获取公共headers + * + * @param body + * @param action + * @return + */ + public Map getPublicHeaders(byte[] body, String action) { + Sign sign = new Sign(region, service, rtcHost, "/", accessKey, secretKey); + try { + return sign.getPublicHeaders("POST", Maps.newHashMap(), body, action, rtcVersion); + } catch (Exception e) { + log.error("getPublicHeaders error", e); + } + return Maps.newHashMap(); + } + + @Override + public String generateRtcToken(String roomId, String userId) { + AccessToken token = new AccessToken(rtcAppId, rtcAppKey, roomId, userId); + //token过期时间 + token.ExpireTime(Utils.getTimestamp() + 3600); + //添加订阅流权限 + token.AddPrivilege(AccessToken.Privileges.PrivSubscribeStream, 0); + //添加发布流权限 + token.AddPrivilege(AccessToken.Privileges.PrivPublishStream, Utils.getTimestamp() + 3600); + return token.Serialize(); + } + + @Override + public void startVoiceChat(String roomId, String taskId, Audio ttsAudio, String systemPrompt, List userPromptList, AgentConfig agentConfig) { + try { + //请求url + String url = String.format(rtcUrl, startVoiceChatAction, rtcVersion); + //请求入参 + StartVoiceChatInput request = StartVoiceChatInput.builder().build(); + //asr配置 + ASRConfig asrConfig = ASRConfig.getAsrConfig(asrAppId, asrAccessToken, asrProvider); + //tts配置 + TTSConfig ttsConfig = TTSConfig.getTTSConfig(ttsAppId, ttsAccessToken, ttsAudio, ttsProvider); + //llm配置 + LLMConfig llmConfig = LLMConfig.getLLMConfig(llmEndPointId, llmApiKey, systemPrompt, userPromptList); + //实时字幕配置 + SubtitleConfig subtitleConfig = SubtitleConfig.getSubtitleConfig(subtitleServiceUrl, subtitleServiceSignature); + //配置 智能体交互服务配置,包括语音识别(ASR)、语音合成(TTS)、大模型(LLM)、字幕和函数调用(Function Calling)配置。 + Config config = Config.getConfig(asrConfig, ttsConfig, llmConfig, subtitleConfig); + //构建完整请求入参 + StartVoiceChatInput startVoiceChatInput = request.buildStartVoiceChatInput(rtcAppId, config, roomId, taskId, agentConfig); + log.info("startVoiceChat url:{}", url); + log.info("startVoiceChat startVoiceChatInput:{}", JSON.toJSONString(startVoiceChatInput)); + //发起请求,获取结果 + String response = getRequestResult(url, startVoiceChatInput, startVoiceChatAction); + log.info("startVoiceChat response: {}", response); + VoiceChatResponse voiceChatResponse = JSONObject.parseObject(response, VoiceChatResponse.class); + if (voiceChatResponse.getResult().equals("ok")) { + log.info("startVoiceChat success"); + } + } catch (Exception e) { + log.error("startVoiceChat error:", e); + } + } + + @Override + public void updateVoiceChat(String roomId, String taskId) { + try { + //请求url + String url = String.format(rtcUrl, updateVoiceChatAction, rtcVersion); + //请求入参 + UpdateVoiceChatInput updateVoiceChatInput = UpdateVoiceChatInput.builder() + .AppId(rtcAppId) + .RoomId(roomId) + .TaskId(taskId) + .Command("interrupt") + .build(); + log.info("updateVoiceChat url: {}", url); + log.info("updateVoiceChat updateVoiceChatInput: {}", JSON.toJSONString(updateVoiceChatInput)); + //发起请求,获取结果 + String response = getRequestResult(url, updateVoiceChatInput, updateVoiceChatAction); + log.info("updateVoiceChat response: {}", response); + VoiceChatResponse voiceChatResponse = JSONObject.parseObject(response, VoiceChatResponse.class); + if (voiceChatResponse.getResult().equals("ok")) { + log.info("updateVoiceChat success"); + } + } catch (Exception e) { + log.error("updateVoiceChat error:", e); + } + } + + @Override + public void stopVoiceChat(String roomId, String taskId) { + try { + //请求url + String url = String.format(rtcUrl, stopVoiceChatAction, rtcVersion); + //请求入参 + StopVoiceChatInput stopVoiceChatInput = StopVoiceChatInput.builder() + .AppId(rtcAppId) + .RoomId(roomId) + .TaskId(taskId) + .build(); + log.info("stopVoiceChat url: {}", url); + log.info("stopVoiceChat stopVoiceChatInput: {}", JSON.toJSONString(stopVoiceChatInput)); + //发起请求,获取结果 + String response = getRequestResult(url, stopVoiceChatInput, stopVoiceChatAction); + log.info("stopVoiceChat response: {}", response); + VoiceChatResponse voiceChatResponse = JSONObject.parseObject(response, VoiceChatResponse.class); + if (voiceChatResponse.getResult().equals("ok")) { + log.info("stopVoiceChat success"); + } + } catch (Exception e) { + log.error("stopVoiceChat error:", e); + } + } + + /** + * 获取请求结果 + * + * @param url + * @param requestBody + * @param action + * @return + * @throws UnirestException + */ + private String getRequestResult(String url, Object requestBody, String action) throws UnirestException { + //请求body转换为bytes数组 + String jsonStr = JSON.toJSONString(requestBody); + log.info("getRequestResult body jsonStr:{}", jsonStr); + byte[] body = jsonStr.getBytes(StandardCharsets.UTF_8); + //获取公共headers + Map publicHeaders = getPublicHeaders(body, action); + log.info("getRequestResult publicHeaders:{}", JSON.toJSONString(publicHeaders)); + log.info("getRequestResult body jsonStr2:{}", jsonStr); + String response = Unirest.post(url) + .headers(publicHeaders) + .body(jsonStr) + .asString().getBody(); + return response; + } + + @Override + public void webhook(HttpServletRequest request, HttpServletResponse response) { + try { + String body = HttpUtil.getBody(request); + log.info("VoiceChatClientImpl webhook body:" + body); + Event event = JSON.parseObject(body, Event.class); + log.info("VoiceChatClientImpl webhookOutput:" + body);//验签 + + response.setContentType("application/json;charset=utf-8"); + if (CheckSignature.check(event, webhookSecret)) { + //发送MQ,相关业务处理 + commonSendMqService.voiceCallWebhookSendMq(event); + response.setStatus(200); + } else { + response.setStatus(500); + } + } catch (Exception e) { + log.error("webhook error:", e); + } + } + + + @Override + public Conv conversationStateCallback(HttpServletRequest request, HttpServletResponse response) { + try { + String body = HttpUtil.getBody(request); + log.info("conversationStateCallback body:" + body); + RtsMessage rtsMessage = JSON.parseObject(body, RtsMessage.class); + String message = rtsMessage.getMessage(); + String signature = rtsMessage.getSignature(); + log.info("conversationStateCallback message:{}", message); + log.info("conversationStateCallback signature:{}", signature); + //验证签名 + if (!"123456".equals(signature)) { + throw new Exception("signature invalid"); + } + Conv conv = ConversationMessageParse.unpack(message); + log.info("conversationStateCallback conv:{}", conv); + return conv; + } catch (Exception e) { + log.error("conversationStateCallback error:", e); + } + return null; + } + + @Override + public Subv rtsCallback(HttpServletRequest request, HttpServletResponse response) { + try { + String body = HttpUtil.getBody(request); + log.info("rtsCallback body:" + body); + RtsMessage rtsMessage = JSON.parseObject(body, RtsMessage.class); + String message = rtsMessage.getMessage(); + String signature = rtsMessage.getSignature(); + log.info("rtsCallback message:{}", message); + log.info("rtsCallback signature:{}", signature); + // 验证签名 + if (!subtitleServiceSignature.equals(signature)) { + throw new Exception("signature invalid"); + } + //解析消息 + Subv subv = MessageParse.unpack(message); + log.info("rtsCallback subv:{}", subv); + return subv; + } catch (Exception e) { + log.error("rtsCallback error:", e.getMessage()); + } + return null; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/asr/AsrInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/asr/AsrInput.java new file mode 100644 index 0000000..bdc02fb --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/asr/AsrInput.java @@ -0,0 +1,96 @@ +package com.sonic.cow.client.input.asr; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +public class AsrInput { + + /** + * 构造函数 + * @param uid + * @param data + */ + public AsrInput(String uid, String data) { + this.user.uid = uid; + this.audio.data = data; + } + + /** + * 用户相关配置 + */ + private User user = new User(); + + /** + * 音频相关配置 + */ + private Audio audio = new Audio(); + + /** + * 请求相关配置 + */ + private Request request = new Request(); + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Audio getAudio() { + return audio; + } + + public void setAudio(Audio audio) { + this.audio = audio; + } + + public Request getRequest() { + return request; + } + + public void setRequest(Request request) { + this.request = request; + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class User { + /** + * 用户标识,必需,可传任意非空字符串,传入值可通过服务端日志追溯 + */ + private String uid; + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class Audio { + + /** + * 语音文件地址 + */ + private String url; + + /** + * base64编码音频内容 + */ + private String data; + + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class Request { + + /** + * 模型名称 + */ + private String model_name = "bigmodel"; + + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/tts/TtsInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/tts/TtsInput.java new file mode 100644 index 0000000..309a675 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/tts/TtsInput.java @@ -0,0 +1,356 @@ +package com.sonic.cow.client.input.tts; + +public class TtsInput { + + public TtsInput() { + } + + public TtsInput(String appId, String cluster, String voiceType) { + this.app.appid = appId; + this.app.cluster = cluster; + this.audio.voice_type = voiceType; + } + + /** + * 应用相关配置 + */ + private App app = new App(); + + /** + * 用户相关配置 + */ + private User user = new User(); + + /** + * 音频相关配置 + */ + private Audio audio = new Audio(); + + /** + * 请求相关配置 + */ + private Request request = new Request(); + + public App getApp() { + return app; + } + + public void setApp(App app) { + this.app = app; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Audio getAudio() { + return audio; + } + + public void setAudio(Audio audio) { + this.audio = audio; + } + + public Request getRequest() { + return request; + } + + public void setRequest(Request request) { + this.request = request; + } + + public static class App { + /** + * 应用标识,必需,需要申请 + */ + private String appid; + + /** + * 应用令牌,必需,可传任意非空字符串,目前未生效,建议使用默认值 + */ + private String token = "access_token"; + + /** + * 业务集群,必需,可选值:volcano_icl 或 volcano_icl_concurr + */ + private String cluster; + + public App() { + + } + + public String getAppid() { + return appid; + } + + public void setAppid(String appid) { + this.appid = appid; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getCluster() { + return cluster; + } + + public void setCluster(String cluster) { + this.cluster = cluster; + } + } + + public static class User { + /** + * 用户标识,必需,可传任意非空字符串,传入值可通过服务端日志追溯 + */ + private String uid; + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + } + + public static class Audio { + /** + * 音色类型,必需,填入以 S_ 开头的声音 ID(SpeakerId),用于语音合成参考音色 + * 备注:声音复刻语音合成请通过下单获取 + */ + private String voice_type; + + /** + * 音频编码格式,可选值:wav / pcm / ogg_opus / mp3,默认 pcm + * 备注:wav 不支持流式 + */ + private String encoding = "pcm"; + + /** + * 音量调节,范围 [0.5, 2],默认 1.0,通常保留一位小数 + * 备注:0.5 表示原音量 0.5 倍,2 表示原音量 2 倍 + */ + private float loudness_ratio = 1.0f; + + /** + * 音频采样率,默认 24000,可选值:8000、16000、24000 + */ + private int rate = 24000; + + /** + * 语速,范围 [0.2, 3],默认 1.0,通常保留一位小数 + */ + private float speed_ratio = 1.0f; + + /** + * 明确语种,非必需,指定后仅读指定语种的文本 + * 可选值: + * - 不设置:正常中英混 + * - crosslingual:启用多语种前端(包含 zh, en, ja, es-mx, id, pt-br) + * - zh:中文为主,支持中英混 + * - en:仅英文 + * - ja:仅日文 + * - es-mx:仅墨西哥西班牙语 + * - id:仅印尼语 + * - pt-br:仅巴西葡萄牙语 + * - de:仅德语(model_type=2 或 3 时支持) + * - fr:仅法语(model_type=2 或 3 时支持) + * 备注: + * - model_type=2(DiT 标准版)建议指定明确语种,支持 zh, en, ja, es-mx, id, pt-br, de, fr + * - model_type=3(DiT 还原版)必须指定明确语种,仅支持 zh, en + */ + private String explicit_language; + + /** + * 参考语种,非必需,给模型提供参考的语种 + * 可选值: + * - 不设置:西欧语种采用英语 + * - id:西欧语种采用印尼语 + * - es:西欧语种采用墨西哥西班牙语 + * - pt:西欧语种采用巴西葡萄牙语 + */ + private String context_language; + + public String getVoice_type() { + return voice_type; + } + + public void setVoice_type(String voice_type) { + this.voice_type = voice_type; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public float getLoudness_ratio() { + return loudness_ratio; + } + + public void setLoudness_ratio(float loudness_ratio) { + this.loudness_ratio = loudness_ratio; + } + + public int getRate() { + return rate; + } + + public void setRate(int rate) { + this.rate = rate; + } + + public float getSpeed_ratio() { + return speed_ratio; + } + + public void setSpeed_ratio(float speed_ratio) { + this.speed_ratio = speed_ratio; + } + + public String getExplicit_language() { + return explicit_language; + } + + public void setExplicit_language(String explicit_language) { + this.explicit_language = explicit_language; + } + + public String getContext_language() { + return context_language; + } + + public void setContext_language(String context_language) { + this.context_language = context_language; + } + } + + public static class Request { + /** + * 请求标识,必需,需要保证每次调用传入值唯一,建议使用 UUID + */ + private String reqid = java.util.UUID.randomUUID().toString(); + + /** + * 合成语音的文本,必需,长度限制 1024 字节(UTF-8 编码) + */ + private String text; + + /** + * 文本类型,可选值:plain / ssml,默认 plain + * 备注:DiT 音色暂不支持 ssml,参考 SSML 标记语言 + */ + private String text_type = "plain"; + + /** + * 时间戳相关,非必需,传入 1 表示启用 + * 备注:启用后返回原文本的时间戳,保留原文中的阿拉伯数字或特殊符号,多个标点连用或空格会被处理但不影响时间戳连贯性 + */ + private int with_timestamp; + + /** + * 操作,必需,可选值:query(非流式,HTTP 只能用 query)/ submit(流式) + */ + private String operation = "query"; + + /** + * 复刻 1.0 语速相关,非必需,传入 1 表示启用 + * 备注:用以解决 1.0 的声音复刻合成时语速过快的情况 + */ + private int split_sentence; + + /** + * 句尾静音,非必需,范围 0~30000ms + * 备注:设置后可在句尾增加静音时长,仅针对传入文本最后的句尾,需在 extra_param 中设置 enable_trailing_silence_audio=true + */ + private float silence_duration; + + /** + * 额外参数,非必需,JSON 格式字符串 + * 支持参数: + * - mute_cut_remain_ms:句首静音参数,需配合 mute_cut_threshold 使用(如 {"mute_cut_threshold":"400","mute_cut_remain_ms":"100"}) + * - disable_emoji_filter:布尔值,默认为 false,开启后 emoji 表情在文本中不过滤显示,建议搭配时间戳使用 + * - unsupported_char_ratio_thresh:浮点数,默认 0.3,最大 1.0,检测不支持语种超过该比例返回错误或兜底音频 + * - cache_config:缓存相关参数,开启后相同文本直接读取缓存(1 小时有效),不附带时间戳(如 {"cache_config":{"text_type":1,"use_cache":true}}) + * 备注:MP3 格式句首存在 100ms 内无法消除的静音,WAV 格式可完全消除句首静音 + */ + private String extra_param; + + public String getReqid() { + return reqid; + } + + public void setReqid(String reqid) { + this.reqid = reqid; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getText_type() { + return text_type; + } + + public void setText_type(String text_type) { + this.text_type = text_type; + } + + public int getWith_timestamp() { + return with_timestamp; + } + + public void setWith_timestamp(int with_timestamp) { + this.with_timestamp = with_timestamp; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public int getSplit_sentence() { + return split_sentence; + } + + public void setSplit_sentence(int split_sentence) { + this.split_sentence = split_sentence; + } + + public float getSilence_duration() { + return silence_duration; + } + + public void setSilence_duration(float silence_duration) { + this.silence_duration = silence_duration; + } + + public String getExtra_param() { + return extra_param; + } + + public void setExtra_param(String extra_param) { + this.extra_param = extra_param; + } + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/StartVoiceChatInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/StartVoiceChatInput.java new file mode 100644 index 0000000..36d6c3e --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/StartVoiceChatInput.java @@ -0,0 +1,64 @@ +package com.sonic.cow.client.input.voicechat; + +import com.alibaba.fastjson.annotation.JSONField; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.google.common.collect.Lists; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import com.sonic.cow.client.input.voicechat.domain.*; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + +/** + * 文档说明:https://www.volcengine.com/docs/6348/1558163 + * + * @author mzc + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class StartVoiceChatInput { + @ApiModelProperty("你的音视频应用的唯一标志,参看创建 RTC 应用获取或创建 AppId。") + @JSONField(name = "AppId") + private String AppId; + @ApiModelProperty("智能体与真人进行通话的房间的 ID,需与真人用户使用客户端 SDK 进房时的使用的 RoomId 保持一致") + @JSONField(name = "RoomId") + private String RoomId; + @ApiModelProperty("智能体任务 ID。由你自行定义,用于标识任务,且后续更新或结束此任务也需要使用该 TaskId。参数定义规则参看参数赋值规范。\n" + + "一个 AppId 的 RoomId 下 TaskId 是唯一的,AppId + RoomId + TaskId 共同构成一个全局唯一的任务标识,用来标识指定 AppId 下某个房间内正在运行的任务,从而能在此任务运行中进行更新或者停止此任务。\n" + + "不同 AppId 或者不同 RoomId 下 TaskId可以重复。") + @JSONField(name = "TaskId") + private String TaskId; + @ApiModelProperty("智能体交互服务配置,包括语音识别(ASR)、语音合成(TTS)、大模型(LLM)、字幕和函数调用(Function Calling)配置。") + @JSONField(name = "Config") + private Config Config; + @ApiModelProperty("智能体相关配置,包括欢迎词、任务状态回调等信息。") + @JSONField(name = "AgentConfig") + private AgentConfig AgentConfig; + + + /** + * 组装开始智能体服务请求对象 + * + * @param rtcAppId + * @param roomId + * @param taskId + * @return + */ + public StartVoiceChatInput buildStartVoiceChatInput(String rtcAppId, Config config, String roomId, String taskId, AgentConfig agentConfig) { + //入参 + StartVoiceChatInput startVoiceChatInput = StartVoiceChatInput.builder() + .AppId(rtcAppId) + .RoomId(roomId) + .TaskId(taskId) + .Config(config) + .AgentConfig(agentConfig) + .build(); + return startVoiceChatInput; + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/StopVoiceChatInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/StopVoiceChatInput.java new file mode 100644 index 0000000..e955f72 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/StopVoiceChatInput.java @@ -0,0 +1,30 @@ +package com.sonic.cow.client.input.voicechat; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 文档说明:https://www.volcengine.com/docs/6348/1558163 + * @author mzc + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class StopVoiceChatInput { + @ApiModelProperty("你的音视频应用的唯一标志,参看创建 RTC 应用获取或创建 AppId。") + @JSONField(name = "AppId") + private String AppId; + @ApiModelProperty("智能体与真人进行通话的房间的 ID,需与真人用户使用客户端 SDK 进房时的使用的 RoomId 保持一致") + @JSONField(name = "RoomId") + private String RoomId; + @ApiModelProperty("智能体任务 ID。由你自行定义,用于标识任务,且后续更新或结束此任务也需要使用该 TaskId。参数定义规则参看参数赋值规范。\n" + + "一个 AppId 的 RoomId 下 TaskId 是唯一的,AppId + RoomId + TaskId 共同构成一个全局唯一的任务标识,用来标识指定 AppId 下某个房间内正在运行的任务,从而能在此任务运行中进行更新或者停止此任务。\n" + + "不同 AppId 或者不同 RoomId 下 TaskId可以重复。") + @JSONField(name = "TaskId") + private String TaskId; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/UpdateVoiceChatInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/UpdateVoiceChatInput.java new file mode 100644 index 0000000..62a3fed --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/UpdateVoiceChatInput.java @@ -0,0 +1,58 @@ +package com.sonic.cow.client.input.voicechat; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 文档说明:https://www.volcengine.com/docs/6348/1558163 + * @author mzc + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UpdateVoiceChatInput { + @ApiModelProperty("你的音视频应用的唯一标志,参看创建 RTC 应用获取或创建 AppId。") + @JSONField(name = "AppId") + private String AppId; + @ApiModelProperty("智能体与真人进行通话的房间的 ID,需与真人用户使用客户端 SDK 进房时的使用的 RoomId 保持一致") + @JSONField(name = "RoomId") + private String RoomId; + @ApiModelProperty("智能体任务 ID。由你自行定义,用于标识任务,且后续更新或结束此任务也需要使用该 TaskId。参数定义规则参看参数赋值规范。\n" + + "一个 AppId 的 RoomId 下 TaskId 是唯一的,AppId + RoomId + TaskId 共同构成一个全局唯一的任务标识,用来标识指定 AppId 下某个房间内正在运行的任务,从而能在此任务运行中进行更新或者停止此任务。\n" + + "不同 AppId 或者不同 RoomId 下 TaskId可以重复。") + @JSONField(name = "TaskId") + private String TaskId; + @ApiModelProperty("更新指令\n" + + "\n" + + "interrupt:打断智能体。\n" + + "function:传回工具调用信息指令。\n" + + "ExternalTextToSpeech : 传入文本信息供 TTS 音频播放。使用方法参看自定义语音播放。\n" + + "ExternalPromptsForLLM:传入自定义文本与用户问题拼接后送入 LLM。\n" + + "ExternalTextToLLM:传入外部问题送入 LLM。根据你设定的优先级决定替代用户问题或增加新一轮对话。\n" + + "FinishSpeechRecognition:触发新一轮对话。") + @JSONField(name = "Command") + private String Command; +// @ApiModelProperty("工具调用信息指令。\n" + +// "\n" + +// "注意\n" + +// "\n" + +// "Command 取值为 function、ExternalTextToSpeech、ExternalPromptsForLLM和ExternalTextToLLM时,Message必填。\n" + +// "当 Command 取值为 function时,Message 格式需为 Json 转译字符串,例如:\n" + +// "\"{\\\"ToolCallID\\\":\\\"call_cx\\\",\\\"Content\\\":\\\"上海天气是台风\\\"}\"\n" + +// "其他取值时格式为普通字符串,例如你刚才的故事讲的真棒。\"\n" + +// "当 Command 取值为 ExternalTextToSpeech时,message 传入内容建议不超过 200 个字符。") +// @JSONField(name = "Message") +// private String Message; +// @ApiModelProperty("传入文本信息或外部问题时,处理的优先级。\n" + +// "\n" + +// "1:高优先级。传入信息直接打断交互,进行处理。\n" + +// "2:中优先级。等待当前交互结束后,进行处理。\n" + +// "3:低优先级。如当前正在发生交互,直接丢弃 Message 传入的信息。\n") +// @JSONField(name = "InterruptMode") +// private Integer InterruptMode=1; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/ASRConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/ASRConfig.java new file mode 100644 index 0000000..ff0f163 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/ASRConfig.java @@ -0,0 +1,75 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 文档:https://www.volcengine.com/docs/6348/1558163 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ASRConfig { + @ApiModelProperty("语音识别服务的提供商。该参数固定取值:volcano,表示仅支持火山引擎语音识别服务。可使用以下模型:\n" + + "火山引擎流式语音识别(识别速度更快)\n" + + "火山引擎流式语音识别大模型(识别准确率更高)\n" + + "两者详细差异(如可识别语种、支持的能力等),请参见流式语音识别和流式语音识别大模型。") + @JSONField(name="Provider") + private String Provider; + + @ApiModelProperty("服务配置参数。\n" + + "不同服务,该结构包含字段不同,具体参看:\n" + + "火山引擎流式语音识别\n" + + "火山引擎流式语音识别大模型") + @JSONField(name="ProviderParams") + private ASRProviderParams ProviderParams; + + @ApiModelProperty("Trigger method for a new round of conversation.\n" + + "0: After the server detects a complete sentence, it automatically triggers a new round of conversation.\n" + + "1: After receiving the input end signal or the speech caption result, you decide whether to trigger a new round of conversation by yourself.\n" + + "The default value is 0") + @JSONField(name="TurnDetectionMode") + private Integer TurnDetectionMode; + + + /** + * 获取ASR配置 + * + * @param asrAppId + * @param asrAccessToken + * @return + */ + public static ASRConfig getAsrConfig(String asrAppId, String asrAccessToken,String asrProvider) { + return ASRConfig.builder() + .Provider(asrProvider) + .ProviderParams(getAsrProviderParams(asrAppId, asrAccessToken)) + .TurnDetectionMode(0) + .build(); + } + /** + * 获取ASR参数 + * + * @param asrAppId + * @param asrAccessToken + * @return + */ + public static ASRProviderParams getAsrProviderParams(String asrAppId, String asrAccessToken) { + return ASRProviderParams.builder() + //模型类型。该参数固定取值:SeedASR,表示火山引擎语音识别大模型 + .Mode("SeedASR") + .Language("en-US") + //开通火山引擎流式语音识别大模型服务后获取的 App ID,用于标识应用。你可登录豆包语音控制台获取。 + .AppId(asrAppId) + //与开通流式语音识别大模型服务 App ID 对应的 AccessToken,用于身份认证。你可登录豆包语音控制台获取。 + .AccessToken(asrAccessToken) + //火山引擎流式语音识别大模型服务开通类型:volc.bigasr.sauc.duration:小时版。volc.bigasr.sauc.concurrent:并发版。 + .StreamMode(0) +// .ApiResourceId("volc.bigasr.sauc.duration") + .build(); + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/ASRProviderParams.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/ASRProviderParams.java new file mode 100644 index 0000000..a772ce9 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/ASRProviderParams.java @@ -0,0 +1,44 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ASRProviderParams { + @ApiModelProperty("必填:模型类型。该参数固定取值:bigmodel,表示火山引擎语音识别大模型。") + @JSONField(name="Mode") + private String Mode; + @ApiModelProperty("必填:AppId String 示例值:93****21\n" + + "开通火山引擎流式语音识别大模型服务后获取的 App ID,用于标识应用。你可登录豆包语音控制台获取。") + @JSONField(name="AppId") + private String AppId; + @ApiModelProperty("必填:与开通流式语音识别大模型服务 App ID 对应的 AccessToken,用于身份认证。你可登录豆包语音控制台获取。\n" + + "Access Token 查找方式,可参看如何获取 Token。") + @JSONField(name="AccessToken") + private String AccessToken; +// @ApiModelProperty("火山引擎流式语音识别大模型服务开通类型:\n" + +// "volc.bigasr.sauc.duration:小时版。\n" + +// "volc.bigasr.sauc.concurrent:并发版。\n" + +// "默认值为 volc.bigasr.sauc.duration。") +// private String ApiResourceId; +@JSONField(name="StreamMode") +@ApiModelProperty("语音识别结果输出模式:\n" + + "0:流式输出。即识别结果会分段、实时地返回。该模式下识别速度更快,适用于实时字幕场景。\n" + + "1:非流式输出。即在完整接收并处理完整个语音片段后,一次性返回最终的识别结果。该模式下识别准确率更高,适用于不需要即时反馈的离线转录场景(如会议录音)。\n" + + "默认值为 0。") + private int StreamMode; + + @ApiModelProperty(" Example: zh-CN\n" + + "Language code, supported values:\n" + + "zh-CN: Chinese (Simplified)\n" + + "en-US: English (United States)") + @JSONField(name="Language") + private String Language; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/AgentConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/AgentConfig.java new file mode 100644 index 0000000..aca4361 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/AgentConfig.java @@ -0,0 +1,37 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AgentConfig { + @ApiModelProperty("真人用户 ID。需使用客户端 SDK 进房的真人用户的 UserId。仅支持传入一个 UserId,即单个房间内,仅支持一个用户与智能体一对一通话。") + @JSONField(name="TargetUserID") + private String[] TargetUserID; + @ApiModelProperty("智能体启动后的欢迎词") + @JSONField(name="WelcomeMessage") + private String WelcomeMessage; + @ApiModelProperty("智能体 ID,用于标识智能体。 由你自行定义、生成与维护,支持由大小写字母(A-Z、a-z)、数字(0-9)、下划线(_)、短横线(-)、句点(.)和 @ 组成,最大长度为 128 个字符。\n" + + "若不填则默认值为 voiceChat_$(TargetUserId)_$(timestamp_now)。") + @JSONField(name="UserId") + private String UserId; + @ApiModelProperty("是否开启会话状态回调。默认值为 false。") + @JSONField(name="EnableConversationStateCallback") + private Boolean EnableConversationStateCallback; + @ApiModelProperty("回调地址配置 可以知道智能体状态 "+ + "1:代理正在监听。\n" + + "2:智能体在思考。\n" + + "3:代理在说话。\n" + + "4:代理被中断。\n" + + "5:代理说完。") + private String ServerMessageURLForRTS; + @ApiModelProperty("用于验证回调源安全性的签名密钥") + private String ServerMessageSignatureForRTS; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/App.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/App.java new file mode 100644 index 0000000..a80fe80 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/App.java @@ -0,0 +1,22 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class App { + @ApiModelProperty("开通火山引擎语音合成服务后获取的 App ID,用于标识应用。你可登录豆包语音控制台获取。") + @JSONField(name="appid") + private String appid; + + @ApiModelProperty("已开通语音合成服务对应的集群标识(Cluster ID)。你可登录豆包语音控制台开通服务后获取。") + @JSONField(name="token") + private String token; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/Audio.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/Audio.java new file mode 100644 index 0000000..52beaa5 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/Audio.java @@ -0,0 +1,28 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Audio { + /** + * String Example: zh_female_cancan_mars_bigtts + * Voice_type of purchased timbre on BytePlus Text-to-Speech Console. + */ + @JSONField(name = "voice_type") + private String voice_type; + /** + * Pitch, limited to [-12,12]. Defaults to 0 + */ + private Integer speech_rate = 0; + /** + * Speech speed, limited to [-50, 100]. Default to 0. + */ + private Integer pitch_rate = 0; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/Config.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/Config.java new file mode 100644 index 0000000..03d6ee2 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/Config.java @@ -0,0 +1,39 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Config { + @ApiModelProperty("语音识别(ASR)相关配置") + @JSONField(name="ASRConfig") + private ASRConfig ASRConfig; + @ApiModelProperty("语音合成(TTS)相关配置") + @JSONField(name="TTSConfig") + private TTSConfig TTSConfig; + @ApiModelProperty("大模型相关配置。支持的大模型平台如下:\n" + + "火山方舟平台\n" + + "Coze平台\n" + + "第三方大模型/Agent") + @JSONField(name="LLMConfig") + private LLMConfig LLMConfig; + @ApiModelProperty("字幕相关配置") + @JSONField(name="SubtitleConfig") + private SubtitleConfig SubtitleConfig; + + public static Config getConfig(ASRConfig asrConfig, TTSConfig ttsConfig, LLMConfig llmConfig, SubtitleConfig subtitleConfig){ + return Config.builder() + .ASRConfig(asrConfig) + .TTSConfig(ttsConfig) + .LLMConfig(llmConfig) + .SubtitleConfig(subtitleConfig) + .build(); + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/LLMConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/LLMConfig.java new file mode 100644 index 0000000..3825e95 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/LLMConfig.java @@ -0,0 +1,94 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.google.common.collect.Lists; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class LLMConfig { + @ApiModelProperty("必填:大模型平台标识。使用火山方舟平台时,该参数固定取值:ArkV3。") + @JSONField(name="Provider") + private String Provider; + @ApiModelProperty("可选 自定义推理接入点 EndPointId。当需要使用模型推理功能(如直接调用部署的基础模型)时,此参数为必填。\n" + + "可前往控制台创建或查询自定义推理接入点。\n" + + "注意\n" + + "EndPointId 与 BotId 不可同时填写,若同时填写,则 EndPointId 生效。\n" + + "当前仅支持自定义推理接入点,不支持预置推理接入点。") + @JSONField(name="EndPointId") + private String EndPointId; + @ApiModelProperty("必填:API 密钥。使用火山方舟平台时,该参数为必填。") + @JSONField(name="APIKey") + private String APIKey; +// @ApiModelProperty("可选 输出文本的最大 token 限制。默认值为 1024。") +// private int MaxTokens; +// @ApiModelProperty("可选 采样温度,用于控制生成文本的随机性和创造性,值越大随机性越高。\n" + +// "取值范围为 (0, 1],默认值为 0.1。") +// private double Temperature; +// @ApiModelProperty("可选 采样的选择范围,控制输出 token 的多样性。模型将从概率分布中累计概率超过该取值的标记中进行采样,以确保采样的选择范围不会过宽,值越大输出的 token 类型越丰富。\n" + +// "取值范围为 [0, 1],默认值为 0.3。") +// private double TopP; + @ApiModelProperty("可选 示例值:[\"你是小宁,性格幽默又善解人意。你在表达时需简明扼要,有自己的观点。\"]\n" + + "系统提示词。用于输入控制大模型行为方式的指令,定义了模型的角色、行为准则,特定的输出格式等。") + @JSONField(name="SystemMessages") + private String[] SystemMessages; + @ApiModelProperty("可选 示例值:[ { \"Role\": \"user\", \"Content\": \"你好\" }, { \"Role\": \"assistant\", \"Content\": \"有什么可以帮到你的?\" }, { \"Role\": \"user\", \"Content\": \"你是谁?\" }, { \"Role\": \"assistant\", \"Content\": \"我是你的智能问答助手。\" }\n" + + "用户提示词,可用于增强模型的回复质量,模型回复时会优先参考此处内容,引导模型生成特定的输出或执行特定的任务。\n" + + "UserPrompts 存储的对话轮数受 HistoryLength 控制。例如 UserPrompts 中预先存储了两轮对话,HistoryLength 设置为 3,用户已进行了三轮对话,第四轮会话开始时,UserPrompts 中存储的内容会被全部删除。\n" + + "注意\n" + + "UserPrompts 中 Role 的取值只包含 user 和 assistant,且必须成对出现,否则大模型可能会出现未定义行为。") + @JSONField(name="UserPrompts") + private UserPrompt[] UserPrompts; +// @ApiModelProperty("可选 示例值:3\n" + +// "历史问题轮数。默认值为 3。\n" + +// "在调用该接口时需要确保所有 UserPrompts 和 SystemMessage 消息文本总长度不超过大模型上下文长度。\n" + +// "例如:历史问题轮数为 3,使用 Skylark2-lite-8k 大模型,长度限制为 8k,UserPrompts 预先存储了两轮对话,用户输入了第一轮会话的问题,此时 SystemMessages+UserPrompts+第一轮会话问题总长度不超过8k。") +// @JSONField(name="HistoryLength") +// private int HistoryLength; + + @ApiModelProperty("关闭或开启大模型的深度思考能力。若你使用的是具备深度思考能力的模型,强烈建议通过该字段关闭模型的深度思考能力(disabled),以避免智能体回复耗时过长,影响对话的流畅性。") + @JSONField(name="ThinkingType") + private String ThinkingType; + + + /** + * 获取LLM配置 + * + * @param llmEndPointId + * @return + */ + public static LLMConfig getLLMConfig(String llmEndPointId, String llmApiKey, String systemPrompt, List userPrompts) { + List systemPromptList = Lists.newArrayList(); + if (StringUtils.isNotEmpty(systemPrompt)) { + systemPromptList.add(systemPrompt); + } + List userPromptList = Lists.newArrayList(); + if (CollectionUtils.isNotEmpty(userPrompts)) { + userPromptList.addAll(userPrompts); + } + return LLMConfig.builder() + .Provider("BytePlus") + .EndPointId(llmEndPointId) + .APIKey(llmApiKey) +// .Temperature(0.1f) +// .MaxTokens(1024) +// .TopP(0.3f) + .SystemMessages(systemPromptList.toArray(new String[0])) + .UserPrompts(userPromptList.toArray(new UserPrompt[0])) + //历史问题轮数。默认值为 3。 +// .HistoryLength(100) + .ThinkingType("disabled") + .build(); + } + +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/SubtitleConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/SubtitleConfig.java new file mode 100644 index 0000000..a10be15 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/SubtitleConfig.java @@ -0,0 +1,39 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-08-25 18:04 + **/ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SubtitleConfig { + @JSONField(name = "DisableRTSSubtitle") + private Boolean DisableRTSSubtitle; + + @JSONField(name = "ServerMessageUrl") + private String ServerMessageUrl; + + @JSONField(name = "ServerMessageSignature") + private String ServerMessageSignature; + + @JSONField(name = "SubtitleMode") + private Integer SubtitleMode = 0; + + public static SubtitleConfig getSubtitleConfig(String serverMessageUrl, String serverMessageSignature) { + return SubtitleConfig.builder() + .DisableRTSSubtitle(false) + .ServerMessageUrl(serverMessageUrl) + .ServerMessageSignature(serverMessageSignature) + .SubtitleMode(0) + .build(); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/TTSConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/TTSConfig.java new file mode 100644 index 0000000..3cbceba --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/TTSConfig.java @@ -0,0 +1,91 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TTSConfig { + +// @ApiModelProperty("过滤大模型返回内容中指定标点符号中的文字后再进行语音合成。你需要在大模型 Prompt 中自行定义哪些内容放在指定标点符号内。具体使用方法参看过滤指定内容。\n" + +// "支持取值及含义如下:\n" + +// "1:中文括号()\n" + +// "2:英文括号()\n" + +// "3:中文方括号【】\n" + +// "4:英文方括号[]\n" + +// "5:英文花括号{}\n" + +// "默认值为空,表示不进行过滤。") +// private String IgnoreBracketText; + + @ApiModelProperty("语音合成服务提供商,使用不同语音合成服务时,取值不同。支持使用的语音合成服务及对应取值如下:\n" + + "volcano(服务自上而下语音生成速度递减,情感表现力递增)\n" + + "火山引擎语音合成\n" + + "火山引擎语音合成大模型(非流式输入流式输出)\n" + + "火山引擎声音复刻大模型(非流式输入流式输出)\n" + + "volcano_bidirection(服务自上而下语音生成速度递减,情感表现力递增)\n" + + "火山引擎语音合成大模型(流式输入流式输出)\n" + + "火山引擎声音复刻大模型(流式输入流式输出)\n" + + "minimax:MiniMax 语音合成\n" + + "ai_gateway:自部署语音合成模型(通过火山边缘大模型网关接入的)") + @JSONField(name="Provider") + private String Provider; + + @ApiModelProperty("配置所选的语音合成服务。不同服务下,该结构包含字段不同:\n" + + "火山引擎语音合成\n" + + "火山引擎语音合成大模型(非流式输入流式输出)\n" + + "火山引擎语音合成大模型(流式输入流式输出)\n" + + "火山引擎声音复刻大模型(非流式输入流式输出)\n" + + "火山引擎声音复刻大模型(流式输入流式输出)\n" + + "MiniMax 语音合成\n" + + "自部署语音合成") + @JSONField(name="ProviderParams") + private TTSProviderParams ProviderParams; + + + /** + * 获取TTS配置 + * + * @param ttsAppId + * @param ttsToken + * @param audio + * @param ttsProvider + * @return + */ + public static TTSConfig getTTSConfig(String ttsAppId, String ttsToken, Audio audio, String ttsProvider) { + TTSProviderParams ttsProviderParams = TTSProviderParams.builder() + .app(getTTSApp(ttsAppId, ttsToken)) + .audio(audio) + //TODO 临时使用 + .ResourceId("volc.megatts.default") +// .ResourceId("volc.service_type.1000009") + .build(); + return TTSConfig.builder() + //语音合成服务提供商,使用不同语音合成服务时,取值不同 待确定 + .Provider(ttsProvider) + .ProviderParams(ttsProviderParams) + .build(); + } + + + /** + * 获取TTS app + * + * @param ttsAppId + * @param ttsToken + * @return + */ + public static App getTTSApp(String ttsAppId, String ttsToken) { + return App.builder() + //开通火山引擎语音合成大模型服务后获取的 App ID,用于标识应用。你可登录豆包语音控制台获取 + .appid(ttsAppId) + //与语音合成大模型服务 App ID 对应的 AccessToken,用于身份认证。你可登录豆包语音控制台获取。Access Token 查找方式,可参看如何获取 Token。 + .token(ttsToken) + .build(); + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/TTSProviderParams.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/TTSProviderParams.java new file mode 100644 index 0000000..5d30b91 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/TTSProviderParams.java @@ -0,0 +1,24 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TTSProviderParams { + @ApiModelProperty("必填:火山引擎语音合成大模型服务应用配置。") + @JSONField(name="app") + private App app; + @ApiModelProperty("必填:火山引擎语音合成大模型服务音频配置") + @JSONField(name="audio") + private Audio audio; + @ApiModelProperty("必填:调用服务的资源信息 ID,该参数固定取值:volc.megatts.default。") + @JSONField(name="ResourceId") + private String ResourceId; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/UserPrompt.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/UserPrompt.java new file mode 100644 index 0000000..d9f91ab --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/input/voicechat/domain/UserPrompt.java @@ -0,0 +1,18 @@ +package com.sonic.cow.client.input.voicechat.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UserPrompt { + @JSONField(name="Role") + private String Role; + @JSONField(name="Content") + private String Content; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/output/tts/TtsOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/output/tts/TtsOutput.java new file mode 100644 index 0000000..5259859 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/output/tts/TtsOutput.java @@ -0,0 +1,47 @@ +package com.sonic.cow.client.output.tts; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class TtsOutput { + + /** + * 请求 ID + */ + private String reqid; + + /** + * 请求状态码 + */ + private String code; + + /** + * 请求状态信息 + */ + private String message; + + /** + * 音频段序号 + */ + private String sequence; + + /** + * 合成音频 + */ + private String data; + + /** + * 额外信息 + */ + private String addition; + + /** + * 音频时长 + */ + private String duration; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/output/voicechat/ResponseMetadata.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/output/voicechat/ResponseMetadata.java new file mode 100644 index 0000000..e5a0f2a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/output/voicechat/ResponseMetadata.java @@ -0,0 +1,12 @@ +package com.sonic.cow.client.output.voicechat; + +import lombok.Data; + +@Data +public class ResponseMetadata { + private String RequestId; + private String Action; + private String Version; + private String Service; + private String Region; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/output/voicechat/VoiceChatResponse.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/output/voicechat/VoiceChatResponse.java new file mode 100644 index 0000000..4094468 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/output/voicechat/VoiceChatResponse.java @@ -0,0 +1,11 @@ +package com.sonic.cow.client.output.voicechat; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class VoiceChatResponse { + @ApiModelProperty("返回值 Result 仅在请求成功时返回 ok,失败时为空") + private String Result; + private ResponseMetadata ResponseMetadata; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Caching.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Caching.java new file mode 100644 index 0000000..dc6bb0a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Caching.java @@ -0,0 +1,24 @@ +package com.sonic.cow.client.request; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Caching { + + /** + * 取值范围:enabled, disabled。 + * enabled:开启缓存。 + * disabled:关闭缓存。 + */ + private String type; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/request/ChatCompletionMessage.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/ChatCompletionMessage.java new file mode 100644 index 0000000..50dfd71 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/ChatCompletionMessage.java @@ -0,0 +1,22 @@ +package com.sonic.cow.client.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-10-21 16:54 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ChatCompletionMessage { + + private String role; + + private String content; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/request/ContentRequest.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/ContentRequest.java new file mode 100644 index 0000000..bb25b49 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/ContentRequest.java @@ -0,0 +1,35 @@ +package com.sonic.cow.client.request; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ContentRequest { + + /** + * 类型:input_text、input_image + */ + private String type; + + /** + * 文本内容(当 type = "input_text" 时,填写内容) + */ + private String text; + + /** + * 图片内容(当 type = "input_image" 时,填写内容) + */ + @JsonProperty("image_url") + private String imageUrl; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Format.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Format.java new file mode 100644 index 0000000..c85f314 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Format.java @@ -0,0 +1,24 @@ +package com.sonic.cow.client.request; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Format { + + /** + * 文本 格式:回复格式的类型,此处应为 text + * JSON 格式:回复格式的类型,此处应为 json_object。 + */ + private String type; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/request/InputRequest.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/InputRequest.java new file mode 100644 index 0000000..7773dc4 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/InputRequest.java @@ -0,0 +1,63 @@ +package com.sonic.cow.client.request; + +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class InputRequest { + + /** + * 角色类型:user, system, assistant, 或者 developer + */ + private ChatMessageRole role; + + /** + * 消息输入的类型,此处应为 message、function_call_output(工具调用结果) + */ + private String type = "message"; + + /** + * 调用工具的id + */ + @JsonProperty("call_id") + private String callId; + + /** + * 调用工具名称 + */ + private String name; + + /** + * 调用工具输入的参数 + */ + private String arguments; + + /** + * 调用工具后输出的结果 + */ + private String output; + + /** + * 项目状态,可选值:in_progress,completed 或 incomplete。 + */ + private String status; + + /** + * 内容列表 + */ + private List content; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/request/ResponseChatCompletionRequest.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/ResponseChatCompletionRequest.java new file mode 100644 index 0000000..9fd0c85 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/ResponseChatCompletionRequest.java @@ -0,0 +1,99 @@ +package com.sonic.cow.client.request; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ResponseChatCompletionRequest { + + /** + * 模型的 ID + */ + private String model; + + /** + * 输入的内容 + */ + private List input; + + /** + * 系统提示词 + */ + private String instructions; + + /** + * 上一次的响应 ID + */ + @JsonProperty("previous_response_id") + private String previousResponseId; + + /** + * 设置存储的有效期,对 store 和 caching 都生效。需传入 UTC Unix 时间戳(单位:秒)。默认值和最大值都是当前时间加 3 天。 + * 注意:不满 1 小时按 1 小时计费。 + */ + @JsonProperty("expire_at") + private Integer expireAt; + + /** + * 模型输出最大 token 数,包含模型回答和思维链内容 + */ + @JsonProperty("max_output_tokens") + private Integer maxOutputTokens; + + /** + * 模型是否开启深度思考模式。默认开启深度思考模式,可以手动关闭 + */ + private Thinking thinking; + + /** + * 是否开启缓存 + */ + private Caching caching; + + /** + * 是否储存生成的模型响应,以便后续通过 API 检索。 + * false:不储存,对话内容不能被后续的 API 检索到。 + * true:储存当前模型响应,对话内容能被后续的 API 检索到。 + */ + private Boolean store; + + /** + * 响应内容是否流式返回: + * false:模型生成完所有内容后一次性返回结果。 + * true:按 SSE 协议逐块返回模型生成内容,并以一条 data: [DONE]消息结束。 + */ + private Boolean stream; + + /** + * 温度值[0-2],默认 1 + */ + private Float temperature; + + /** + * 采样概率 + */ + @JsonProperty("top_p") + private Float topP; + + /** + * 模型文本输出的格式定义,可以是自然语言,也可以是结构化的 JSON 数据。详情请看结构化输出。 + */ + private Text text; + + /** + * 模型可以调用的工具列表 + */ + private List tools; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Text.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Text.java new file mode 100644 index 0000000..7b9ac6d --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Text.java @@ -0,0 +1,20 @@ +package com.sonic.cow.client.request; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Text { + + private Format format; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Thinking.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Thinking.java new file mode 100644 index 0000000..86bd65c --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Thinking.java @@ -0,0 +1,26 @@ +package com.sonic.cow.client.request; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Thinking { + + /** + * 取值范围:enabled, disabled,auto。 + * enabled:开启思考模式,模型一定先思考后回答。 + * disabled:关闭思考模式,模型直接回答问题,不会进行思考。 + * auto:自动思考模式,模型根据问题自主判断是否需要思考,简单题目直接回答 + */ + private String type; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Tools.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Tools.java new file mode 100644 index 0000000..4f22c7a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/request/Tools.java @@ -0,0 +1,38 @@ +package com.sonic.cow.client.request; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Tools { + + /** + * 可以调用的函数名称 + */ + private String name; + + /** + * 工具调用的类型,默认为function + */ + private String type = "function"; + + /** + * 被调用函数的描述 + */ + private String description; + + /** + * JSON Schema 格式的函数的请求参数 + */ + private String parameters; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/response/ResponseChatResponse.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/response/ResponseChatResponse.java new file mode 100644 index 0000000..8ff9bbb --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/response/ResponseChatResponse.java @@ -0,0 +1,39 @@ +package com.sonic.cow.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ResponseChatResponse { + + private String responseId; + + private String message; + + private BigDecimal score; + + /** + * 异常码 + */ + private String code; + + /** + * message: 正常消息 + * function_call: 函数调用 + */ + private String type; + + private String functionCallId; + + private String functionCallName; + + private String functionCallArguments; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/AccessToken.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/AccessToken.java new file mode 100644 index 0000000..099fd30 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/AccessToken.java @@ -0,0 +1,156 @@ +package com.sonic.cow.client.rtc; + +import java.util.Arrays; +import java.util.TreeMap; + + +public class AccessToken { + public enum Privileges { + PrivPublishStream(0), + + // not exported, do not use directly + privPublishAudioStream(1), + privPublishVideoStream(2), + privPublishDataStream(3), + + PrivSubscribeStream(4); + + public short intValue; + + Privileges(int value) { + intValue = (short) value; + + } + } + + public String appID; + public String appKey; + public String roomID; + public String userID; + public int issuedAt; + public int expireAt; + public int nonce; + public TreeMap privileges; + + public byte[] signature; + + // Initializes token struct by required parameters. + public AccessToken(String appID, String appKey, String roomID, String userID){ + this.appID = appID; + this.appKey = appKey; + this.roomID = roomID; + this.userID = userID; + this.issuedAt = Utils.getTimestamp(); + this.nonce = Utils.randomInt(); + this.privileges = new TreeMap<>(); + } + + public static String getVersion() { + return "001"; + } + + // AddPrivilege adds permission for token with an expiration. + public void AddPrivilege(Privileges privilege, int expireTimestamp){ + this.privileges.put(privilege.intValue, expireTimestamp); + + if (privilege.intValue == Privileges.PrivPublishStream.intValue){ + this.privileges.put(Privileges.privPublishVideoStream.intValue, expireTimestamp); + this.privileges.put(Privileges.privPublishAudioStream.intValue, expireTimestamp); + this.privileges.put(Privileges.privPublishDataStream.intValue, expireTimestamp); + } + } + + // ExpireTime sets token expire time, won't expire by default. + // The token will be invalid after expireTime no matter what privilege's expireTime is. + public void ExpireTime(int expireTimestamp){ + this.expireAt = expireTimestamp; + } + + public byte[] packMsg() { + ByteBuf buffer = new ByteBuf(); + + return buffer.put(this.nonce).put(this.issuedAt).put(this.expireAt).put(this.roomID).put(this.userID).putIntMap(this.privileges).asBytes(); + } + + // Serialize generates the token string + public String Serialize(){ + byte[] msg = this.packMsg(); + try{ + this.signature = Utils.hmacSign(this.appKey, msg); + }catch(Exception e){ + e.printStackTrace(); + } + + ByteBuf buffer = new ByteBuf(); + byte[] content = buffer.put(msg).put(signature).asBytes(); + + return getVersion() + this.appID + Utils.base64Encode(content); + } + + // Verify checks if this token valid, called by server side. + public boolean Verify(String key){ + if (this.expireAt > 0 && Utils.getTimestamp() > this.expireAt){ + return false; + } + + this.appKey = key; + + byte[] signature; + + try{ + signature = Utils.hmacSign(this.appKey, this.packMsg()); + }catch(Exception e){ + e.printStackTrace(); + return false; + } + + return Arrays.equals(this.signature,signature); + } + + // Parse retrieves token information from raw string + public static AccessToken Parse(String raw){ + AccessToken token = new AccessToken("", "", "", ""); + + if (raw.length() <= Utils.VERSION_LENGTH + Utils.APP_ID_LENGTH){ + return token; + } + + if (!getVersion().equals(raw.substring(0, Utils.VERSION_LENGTH))) { + return token; + } + + token.appID = raw.substring(Utils.VERSION_LENGTH, Utils.VERSION_LENGTH + Utils.APP_ID_LENGTH); + byte[] content = Utils.base64Decode(raw.substring(Utils.VERSION_LENGTH + Utils.APP_ID_LENGTH, raw.length())); + + ByteBuf buffer = new ByteBuf(content); + byte[] msg = buffer.readBytes(); + token.signature = buffer.readBytes(); + + ByteBuf msgBuf = new ByteBuf(msg); + token.nonce = msgBuf.readInt(); + token.issuedAt = msgBuf.readInt(); + token.expireAt = msgBuf.readInt(); + token.roomID = msgBuf.readString(); + token.userID = msgBuf.readString(); + token.privileges = msgBuf.readIntMap(); + + return token; + } + + @Override + public String toString() { + return "AccessToken{" + + "appID='" + appID + '\'' + + ", appKey='" + appKey + '\'' + + ", roomID='" + roomID + '\'' + + ", userID='" + userID + '\'' + + ", issuedAt=" + issuedAt + + ", expireAt=" + expireAt + + ", nonce=" + nonce + + ", privileges=" + privileges + + ", signature=" + Arrays.toString(signature) + + '}'; + } + +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/AccessTokenTest.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/AccessTokenTest.java new file mode 100644 index 0000000..022d852 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/AccessTokenTest.java @@ -0,0 +1,25 @@ +package com.sonic.cow.client.rtc; + +// cd java/src/main/java/io/rtc && javac *.java +// cd java/src/main/java && java io.rtc.AccessTokenTest +class AccessTokenTest { + + public static void main(String[] args) { + AccessToken token = new AccessToken("123456781234567812345678", "app key", "new room", "new user id"); + token.ExpireTime(Utils.getTimestamp() + 3600); + token.AddPrivilege(AccessToken.Privileges.PrivSubscribeStream, 0); + token.AddPrivilege(AccessToken.Privileges.PrivPublishStream, Utils.getTimestamp() + 3600); + + + String s = token.Serialize(); + System.out.println(s); + + System.out.println(token); + + AccessToken t = AccessToken.Parse(s); + System.out.println(t); + + System.out.println(t.Verify("app key")); + + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/ByteBuf.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/ByteBuf.java new file mode 100644 index 0000000..3002a8b --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/ByteBuf.java @@ -0,0 +1,122 @@ +package com.sonic.cow.client.rtc; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Map; +import java.util.TreeMap; + +public class ByteBuf { + ByteBuffer buffer = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN); + + public ByteBuf() { + } + + public ByteBuf(byte[] bytes) { + this.buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + } + + public byte[] asBytes() { + byte[] out = new byte[buffer.position()]; + buffer.rewind(); + buffer.get(out, 0, out.length); + return out; + } + + // packUint16 + public ByteBuf put(short v) { + buffer.putShort(v); + return this; + } + + public ByteBuf put(byte[] v) { + put((short)v.length); + buffer.put(v); + return this; + } + + // packUint32 + public ByteBuf put(int v) { + buffer.putInt(v); + return this; + } + + public ByteBuf put(long v) { + buffer.putLong(v); + return this; + } + + public ByteBuf put(String v) { + return put(v.getBytes()); + } + + public ByteBuf put(TreeMap extra) { + put((short)extra.size()); + + for (Map.Entry pair : extra.entrySet()) { + put(pair.getKey()); + put(pair.getValue()); + } + + return this; + } + + public ByteBuf putIntMap(TreeMap extra) { + put((short)extra.size()); + + for (Map.Entry pair : extra.entrySet()) { + put(pair.getKey()); + put(pair.getValue()); + } + + return this; + } + + public short readShort() { + return buffer.getShort(); + } + + + public int readInt() { + return buffer.getInt(); + } + + public byte[] readBytes() { + short length = readShort(); + byte[] bytes = new byte[length]; + buffer.get(bytes); + return bytes; + } + + public String readString() { + byte[] bytes = readBytes(); + return new String(bytes); + } + + public TreeMap readMap() { + TreeMap map = new TreeMap<>(); + + short length = readShort(); + + for (short i = 0; i < length; ++i) { + short k = readShort(); + String v = readString(); + map.put(k, v); + } + + return map; + } + + public TreeMap readIntMap() { + TreeMap map = new TreeMap<>(); + + short length = readShort(); + + for (short i = 0; i < length; ++i) { + short k = readShort(); + Integer v = readInt(); + map.put(k, v); + } + + return map; + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/CheckSignature.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/CheckSignature.java new file mode 100644 index 0000000..3af515f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/CheckSignature.java @@ -0,0 +1,68 @@ +package com.sonic.cow.client.rtc; + +import com.google.common.collect.Lists; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.List; + +/** + * @description: 回调验签 + * @author: mzc + * @date: 2025-08-04 15:00 + **/ +public class CheckSignature { + + public static void main(String[] args) { + Event event = new Event(); + event.EventType = "RoomCreate"; + event.EventData = "{\"RoomId\":\"room1\",\"Timestamp\":1679383924691}"; + event.EventTime = "2023-03-21T15:32:04+08:00"; + event.EventId = "123456"; + event.Version = "2020-12-01"; + event.AppId = "appId"; + event.Nonce = "aaBc"; + event.Signature = "1c7200723842eff514b65fc3f065597432bbb4249e10d33db79b3853d05f3691"; + String secretKey = "1234"; + + boolean ret = check(event, secretKey); + System.out.println(ret); + + } + + public static boolean check(Event event, String secretKey) { + try { + List data = Lists.newArrayList(); + data.add(event.EventType); + data.add(event.EventData); + data.add(event.EventTime); + data.add(event.EventId); + data.add(event.AppId); + data.add(event.Version); + data.add(event.Nonce); + data.add(secretKey); + + Collections.sort(data); + + final String payloadData = String.join("", data); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(payloadData.getBytes()); + String signature = byteToHexString(digest.digest()); + System.out.println(signature); + + if (event.Signature.equals(signature)) { + return true; + } + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return false; + } + + private static String byteToHexString(byte[] bytes) { + return String.format("%064x", new BigInteger(1, bytes)); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/Event.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/Event.java new file mode 100644 index 0000000..ba2368a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/Event.java @@ -0,0 +1,30 @@ +package com.sonic.cow.client.rtc; + +import lombok.Data; + +/** + * @description: 回调内容 + * @author: mzc + * @date: 2025-08-04 14:56 + **/ +@Data +public class Event { + + //参数名 类型 示例值 描述 + //EventType String RecordStarted 事件类型 + //EventData String / 具体的事件内容,格式为 Json + //EventTime String 1970-07-01T00:00:00Z 事件产生时间,日期格式遵守 ISO-8601 标准。 + //EventId String / 事件 Id,具有唯一性,可用于去重 + //AppId String Your_AppId RTC 应用的唯一标识 + //Version String 2020-12-01 事件的版本号 + //Signature String / 回调签名。 + //Nonce String / 签名随机数 4位 + public String EventType; + public String EventData; + public String EventTime; + public String EventId; + public String AppId; + public String Version; + public String Signature; + public String Nonce; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/EventData.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/EventData.java new file mode 100644 index 0000000..8a13a0c --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/EventData.java @@ -0,0 +1,18 @@ +package com.sonic.cow.client.rtc; + +import lombok.Data; + +/** + * @description: + * @author: mzc + * @date: 2025-09-15 18:51 + **/ +@Data +public class EventData { + + private String RoomId; + + private Long Timestamp; + + private String UserId; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/HexFormat.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/HexFormat.java new file mode 100644 index 0000000..42b4895 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/HexFormat.java @@ -0,0 +1,84 @@ +package com.sonic.cow.client.rtc; + +/** + * @description: + * @author: mzc + * @date: 2025-08-01 14:29 + **/ +/** + * 兼容 Java 16 及以下版本的十六进制编码/解码工具类 + */ +public class HexFormat { + + // 小写十六进制字符集 + private static final char[] LOWER_CASE_HEX = "0123456789abcdef".toCharArray(); + // 大写十六进制字符集 + private static final char[] UPPER_CASE_HEX = "0123456789ABCDEF".toCharArray(); + + // 是否使用大写字母(默认小写) + private final boolean upperCase; + + // 私有构造器,通过 of() 方法创建实例 + private HexFormat(boolean upperCase) { + this.upperCase = upperCase; + } + + /** + * 创建默认的 HexFormat 实例(小写字母) + */ + public static HexFormat of() { + return new HexFormat(false); + } + + /** + * 创建使用大写字母的 HexFormat 实例 + */ + public static HexFormat ofUpperCase() { + return new HexFormat(true); + } + + /** + * 将字节数组转换为十六进制字符串 + * @param bytes 输入字节数组(非空) + * @return 十六进制字符串(长度为字节数组长度的2倍) + */ + public String formatHex(byte[] bytes) { + if (bytes == null) { + throw new IllegalArgumentException("字节数组不能为null"); + } + char[] hexChars = new char[bytes.length * 2]; + char[] hexTable = upperCase ? UPPER_CASE_HEX : LOWER_CASE_HEX; + + for (int i = 0; i < bytes.length; i++) { + // 取字节的高4位(无符号) + int high = (bytes[i] & 0xF0) >>> 4; + // 取字节的低4位(无符号) + int low = bytes[i] & 0x0F; + // 转换为十六进制字符 + hexChars[i * 2] = hexTable[high]; + hexChars[i * 2 + 1] = hexTable[low]; + } + return new String(hexChars); + } + + /** + * 将十六进制字符串转换为字节数组 + * @param hex 十六进制字符串(长度必须为偶数,仅包含0-9、a-f、A-F) + * @return 字节数组 + */ + public byte[] parseHex(String hex) { + if (hex == null || hex.length() % 2 != 0) { + throw new IllegalArgumentException("十六进制字符串必须为非空且长度为偶数"); + } + + byte[] bytes = new byte[hex.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + // 截取每两个字符作为一个字节 + int start = i * 2; + String hexByte = hex.substring(start, start + 2); + // 转换为字节(16进制转10进制,再强转为byte) + bytes[i] = (byte) Integer.parseInt(hexByte, 16); + } + return bytes; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/Sign.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/Sign.java new file mode 100644 index 0000000..d302717 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/Sign.java @@ -0,0 +1,182 @@ +package com.sonic.cow.client.rtc; + +import com.google.common.collect.Maps; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * @description: + * @author: mzc + * @date: 2025-08-01 14:11 + **/ +public class Sign { + private static final BitSet URLENCODER = new BitSet(256); + + private static final String CONST_ENCODE = "0123456789ABCDEF"; + public static final Charset UTF_8 = StandardCharsets.UTF_8; + + static { + int i; + for (i = 97; i <= 122; ++i) { + URLENCODER.set(i); + } + + for (i = 65; i <= 90; ++i) { + URLENCODER.set(i); + } + + for (i = 48; i <= 57; ++i) { + URLENCODER.set(i); + } + URLENCODER.set('-'); + URLENCODER.set('_'); + URLENCODER.set('.'); + URLENCODER.set('~'); + } + + private final String region; + private final String service; + private final String host; + private final String path; + private final String accessKey; + private final String secretKey; + + public Sign(String region, String service, String host, String path, String accessKey, String secretKey) { + this.region = region; + this.service = service; + this.host = host; + this.path = path; + this.accessKey = accessKey; + this.secretKey = secretKey; + } + + /** + * 获取公共请求头 + * + * @param method + * @param queryList + * @param body + * @param action + * @param version + * @return + * @throws Exception + */ + public Map getPublicHeaders(String method, Map queryList, byte[] body, String action, String version) throws Exception { + Date date = new Date(); + + if (body == null) { + body = new byte[0]; + } + String xContentSha256 = hashSHA256(body); + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + String xDate = sdf.format(date); + String shortXDate = xDate.substring(0, 8); + String contentType = "application/json"; + + String signHeader = "host;x-date;x-content-sha256;content-type"; + + + SortedMap realQueryList = new TreeMap<>(queryList); + realQueryList.put("Action", action); + realQueryList.put("Version", version); + StringBuilder querySB = new StringBuilder(); + for (String key : realQueryList.keySet()) { + querySB.append(signStringEncoder(key)).append("=").append(signStringEncoder(realQueryList.get(key))).append("&"); + } + querySB.deleteCharAt(querySB.length() - 1); + + String canonicalStringBuilder = method + "\n" + path + "\n" + querySB + "\n" + + "host:" + host + "\n" + + "x-date:" + xDate + "\n" + + "x-content-sha256:" + xContentSha256 + "\n" + + "content-type:" + contentType + "\n" + + "\n" + + signHeader + "\n" + + xContentSha256; + + System.out.println(canonicalStringBuilder); + + String hashcanonicalString = hashSHA256(canonicalStringBuilder.getBytes()); + String credentialScope = shortXDate + "/" + region + "/" + service + "/request"; + String signString = "HMAC-SHA256" + "\n" + xDate + "\n" + credentialScope + "\n" + hashcanonicalString; + + byte[] signKey = genSigningSecretKeyV4(secretKey, shortXDate, region, service); + String signature = HexFormat.of().formatHex(hmacSHA256(signKey, signString)); + + Map headers = Maps.newHashMap(); + headers.put("Host", host); + headers.put("Content-Type", "application/json"); + headers.put("X-Date", xDate); + headers.put("X-Content-Sha256", xContentSha256); + headers.put("Authorization", "HMAC-SHA256" + + " Credential=" + accessKey + "/" + credentialScope + + ", SignedHeaders=" + signHeader + + ", Signature=" + signature); + return headers; + } + + private String signStringEncoder(String source) { + if (source == null) { + return null; + } + StringBuilder buf = new StringBuilder(source.length()); + ByteBuffer bb = UTF_8.encode(source); + while (bb.hasRemaining()) { + int b = bb.get() & 255; + if (URLENCODER.get(b)) { + buf.append((char) b); + } else if (b == 32) { + buf.append("%20"); + } else { + buf.append("%"); + char hex1 = CONST_ENCODE.charAt(b >> 4); + char hex2 = CONST_ENCODE.charAt(b & 15); + buf.append(hex1); + buf.append(hex2); + } + } + + return buf.toString(); + } + + public static String hashSHA256(byte[] content) throws Exception { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + + return HexFormat.of().formatHex(md.digest(content)); + } catch (Exception e) { + throw new Exception( + "Unable to compute hash while signing request: " + + e.getMessage(), e); + } + } + + public static byte[] hmacSHA256(byte[] key, String content) throws Exception { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(key, "HmacSHA256")); + return mac.doFinal(content.getBytes()); + } catch (Exception e) { + throw new Exception( + "Unable to calculate a request signature: " + + e.getMessage(), e); + } + } + + private byte[] genSigningSecretKeyV4(String secretKey, String date, String region, String service) throws Exception { + byte[] kDate = hmacSHA256((secretKey).getBytes(), date); + byte[] kRegion = hmacSHA256(kDate, region); + byte[] kService = hmacSHA256(kRegion, service); + return hmacSHA256(kService, "request"); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/SignTest.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/SignTest.java new file mode 100644 index 0000000..7021b5b --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/SignTest.java @@ -0,0 +1,250 @@ +package com.sonic.cow.client.rtc; + + +import com.alibaba.fastjson.JSON; +import com.sonic.cow.client.input.voicechat.StopVoiceChatInput; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Copyright (year) Beijing Volcano Engine Technology Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +public class SignTest { + + private static final BitSet URLENCODER = new BitSet(256); + + private static final String CONST_ENCODE = "0123456789ABCDEF"; + public static final Charset UTF_8 = StandardCharsets.UTF_8; + + private String region; + private String service; + private String schema; + private String host; + private String path; + private String ak; + private String sk; + + static { + int i; + for (i = 97; i <= 122; ++i) { + URLENCODER.set(i); + } + + for (i = 65; i <= 90; ++i) { + URLENCODER.set(i); + } + + for (i = 48; i <= 57; ++i) { + URLENCODER.set(i); + } + URLENCODER.set('-'); + URLENCODER.set('_'); + URLENCODER.set('.'); + URLENCODER.set('~'); + } + + public SignTest(String region, String service, String schema, String host, String path, String ak, String sk) { + this.region = region; + this.service = service; + this.host = host; + this.schema = schema; + this.path = path; + this.ak = ak; + this.sk = sk; + } + + public static void main(String[] args) throws Exception { + String SecretAccessKey = "TkdVMFpHUTJNall4TkRJNU5HUTRZMkZqT1dVNVpHSXdaR1ptT1RjNFl6aw=="; + String AccessKeyID = "AKAPNjdlODVmYzJjNGU4NGU5Njg0M2FhNWRiM2U2ZjY0YTI"; + // 请求地址 + String endpoint = "open.byteplusapi.com"; + String path = "/"; // 路径,不包含 Query// 请求接口信息 + String service = "rtc"; + String region = "ap-singapore-1"; + String schema = "https"; + SignTest sign = new SignTest(region, service, schema, endpoint, path, AccessKeyID, SecretAccessKey); + +// String action = "StartVoiceChat"; + String action = "StopVoiceChat"; + String version = "2025-05-01"; + + Date date = new Date(); + HashMap queryMap = new HashMap(); + +// StopVoiceChatInput stopVoiceChatInput = StopVoiceChatInput.builder() +// .AppId("689ade491323ae01797818e0") +// .RoomId("439058245812225-439059452002305") +// .TaskId("439058245812225-439059452002305-1756218710552") +// .build(); + String jsonStr = "{\"AppId\":\"689ade491323ae01797818e0\",\"Command\":\"interrupt\",\"RoomId\":\"439058245812225-439257063882753\",\"TaskId\":\"439058245812225-439257063882753-1756456315405\"}"; + byte[] body = jsonStr.getBytes(StandardCharsets.UTF_8); + sign.doRequest("POST", queryMap, body, date, action, version); + } + + public void doRequest(String method, Map queryList, byte[] body, + Date date, String action, String version) throws Exception { + if (body == null) { + body = new byte[0]; + } + String xContentSha256 = hashSHA256(body); + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + String xDate = sdf.format(date); + String shortXDate = xDate.substring(0, 8); + String contentType = "application/json"; + + String signHeader = "host;x-date;x-content-sha256;content-type"; + + + SortedMap realQueryList = new TreeMap<>(queryList); + realQueryList.put("Action", action); + realQueryList.put("Version", version); + StringBuilder querySB = new StringBuilder(); + for (String key : realQueryList.keySet()) { + querySB.append(signStringEncoder(key)).append("=").append(signStringEncoder(realQueryList.get(key))).append("&"); + } + querySB.deleteCharAt(querySB.length() - 1); + + String canonicalStringBuilder = method + "\n" + path + "\n" + querySB + "\n" + + "host:" + host + "\n" + + "x-date:" + xDate + "\n" + + "x-content-sha256:" + xContentSha256 + "\n" + + "content-type:" + contentType + "\n" + + "\n" + + signHeader + "\n" + + xContentSha256; + + System.out.println(canonicalStringBuilder); + + String hashcanonicalString = hashSHA256(canonicalStringBuilder.getBytes()); + String credentialScope = shortXDate + "/" + region + "/" + service + "/request"; + String signString = "HMAC-SHA256" + "\n" + xDate + "\n" + credentialScope + "\n" + hashcanonicalString; + + byte[] signKey = genSigningSecretKeyV4(sk, shortXDate, region, service); + String signature = HexFormat.of().formatHex(hmacSHA256(signKey, signString)); + + + URL url = new URL(schema + "://" + host + path + "?" + querySB); + + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + conn.setRequestProperty("Host", host); + conn.setRequestProperty("X-Date", xDate); + conn.setRequestProperty("X-Content-Sha256", xContentSha256); + conn.setRequestProperty("Content-Type", contentType); + conn.setRequestProperty("Authorization", "HMAC-SHA256" + + " Credential=" + ak + "/" + credentialScope + + ", SignedHeaders=" + signHeader + + ", Signature=" + signature); + if (!Objects.equals(conn.getRequestMethod(), "GET")) { + conn.setDoOutput(true); + OutputStream os = conn.getOutputStream(); + os.write(body); + os.flush(); + os.close(); + } + conn.connect(); + + int responseCode = conn.getResponseCode(); + + InputStream is; + if (responseCode == 200) { + is = conn.getInputStream(); + } else { + is = conn.getErrorStream(); + } + // 手动读取流(兼容 Java 8 及以下) + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len; + while ((len = is.read(buffer)) != -1) { + bos.write(buffer, 0, len); + } + is.close(); // 关闭流 + bos.close(); + + String responseBody = bos.toString(StandardCharsets.UTF_8.name()); // 指定编码,避免乱码 + System.out.println(responseCode); + System.out.println(responseBody); + } + + private String signStringEncoder(String source) { + if (source == null) { + return null; + } + StringBuilder buf = new StringBuilder(source.length()); + ByteBuffer bb = UTF_8.encode(source); + while (bb.hasRemaining()) { + int b = bb.get() & 255; + if (URLENCODER.get(b)) { + buf.append((char) b); + } else if (b == 32) { + buf.append("%20"); + } else { + buf.append("%"); + char hex1 = CONST_ENCODE.charAt(b >> 4); + char hex2 = CONST_ENCODE.charAt(b & 15); + buf.append(hex1); + buf.append(hex2); + } + } + + return buf.toString(); + } + + public static String hashSHA256(byte[] content) throws Exception { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + + return HexFormat.of().formatHex(md.digest(content)); + } catch (Exception e) { + throw new Exception( + "Unable to compute hash while signing request: " + + e.getMessage(), e); + } + } + + public static byte[] hmacSHA256(byte[] key, String content) throws Exception { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(key, "HmacSHA256")); + return mac.doFinal(content.getBytes()); + } catch (Exception e) { + throw new Exception( + "Unable to calculate a request signature: " + + e.getMessage(), e); + } + } + + private byte[] genSigningSecretKeyV4(String secretKey, String date, String region, String service) throws Exception { + byte[] kDate = hmacSHA256((secretKey).getBytes(), date); + byte[] kRegion = hmacSHA256(kDate, region); + byte[] kService = hmacSHA256(kRegion, service); + return hmacSHA256(kService, "request"); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/Utils.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/Utils.java new file mode 100644 index 0000000..9df91f1 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/Utils.java @@ -0,0 +1,40 @@ +package com.sonic.cow.client.rtc; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Date; + +public class Utils { + public static final long HMAC_SHA256_LENGTH = 32; + public static final int VERSION_LENGTH = 3; + public static final int APP_ID_LENGTH = 24; + + public static byte[] hmacSign(String keyString, byte[] msg) throws InvalidKeyException, NoSuchAlgorithmException { + SecretKeySpec keySpec = new SecretKeySpec(keyString.getBytes(), "HmacSHA256"); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(keySpec); + return mac.doFinal(msg); + } + + public static String base64Encode(byte[] data) { + byte[] encodedBytes = Base64.getEncoder().encode(data); + return new String(encodedBytes); + } + + public static byte[] base64Decode(String data) { + return Base64.getDecoder().decode(data.getBytes()); + } + + public static int getTimestamp() { + return (int)((new Date().getTime())/1000); + } + + public static int randomInt() { + return new SecureRandom().nextInt(); + } + +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/RtsMessage.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/RtsMessage.java new file mode 100644 index 0000000..772964f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/RtsMessage.java @@ -0,0 +1,15 @@ +package com.sonic.cow.client.rtc.callback; + +import lombok.Data; + +/** + * @description: 会话状态回调消息 + * @author: mzc + * @date: 2025-08-24 17:18 + **/ +@Data +public class RtsMessage { + private String message; + private String signature; +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/Conv.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/Conv.java new file mode 100644 index 0000000..f37b877 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/Conv.java @@ -0,0 +1,28 @@ +package com.sonic.cow.client.rtc.callback.conversation; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * @description: + * @author: mzc + * @date: 2025-08-24 17:12 + **/ +@Data +public class Conv { + @ApiModelProperty("TaskId") + private String TaskId; + + @ApiModelProperty("UserID") + private String UserID; + + @ApiModelProperty("RoundID") + private long RoundID; + + @ApiModelProperty("EventTime") + private long EventTime; + + @ApiModelProperty("Stage") + private StageInfo Stage; +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/ConversationMessageParse.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/ConversationMessageParse.java new file mode 100644 index 0000000..19c981a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/ConversationMessageParse.java @@ -0,0 +1,41 @@ +package com.sonic.cow.client.rtc.callback.conversation; + +import com.alibaba.fastjson.JSON; +import com.sonic.cow.client.rtc.callback.rts.Subv; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * @description: 会话状态消息解析 + * @author: mzc + * @date: 2025-08-27 15:20 + **/ +public class ConversationMessageParse { + + public static Conv unpack(String msg) throws Exception { + //Base64解码 + byte[] data = Base64.getDecoder().decode(msg); + // 验证数据长度 + if (data.length < 8) { + throw new Exception("Data invalid"); + } + // 验证头部 + String dataHeader = new String(data, 0, 4, StandardCharsets.UTF_8); + if (!"conv".equals(dataHeader)) { + throw new Exception("Header not match"); + } + // 验证数据大小 + ByteBuffer buffer = ByteBuffer.wrap(data, 4, 4); + int dataSize = buffer.getInt(); // BigEndian解码 + if (dataSize + 8 != data.length) { + throw new Exception("Size not match"); + } + // 解析JSON内容 + byte[] subData = new byte[dataSize]; + System.arraycopy(data, 8, subData, 0, dataSize); + + return JSON.parseObject(subData, Conv.class); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/StageCode.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/StageCode.java new file mode 100644 index 0000000..2b285eb --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/StageCode.java @@ -0,0 +1,26 @@ +package com.sonic.cow.client.rtc.callback.conversation; + +import lombok.Getter; + +/** + * @description: + * @author: mzc + * @date: 2025-08-24 17:13 + **/ +@Getter +public enum StageCode { + LISTENING(1, "listening"), + THINKING(2, "thinking"), + ANSWERING(3, "answering"), + INTERRUPTED(4, "interrupted"), + ANSWER_FINISH(5, "answerFinish"); + + private final int code; + private final String description; + + StageCode(int code, String description) { + this.code = code; + this.description = description; + } +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/StageInfo.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/StageInfo.java new file mode 100644 index 0000000..fc82f0f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/conversation/StageInfo.java @@ -0,0 +1,15 @@ +package com.sonic.cow.client.rtc.callback.conversation; + +import lombok.Data; + +/** + * @description: + * @author: mzc + * @date: 2025-08-24 17:15 + **/ +@Data +public class StageInfo { + private Integer Code; + private String Description; +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/rts/Data.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/rts/Data.java new file mode 100644 index 0000000..c32a7cc --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/rts/Data.java @@ -0,0 +1,41 @@ +package com.sonic.cow.client.rtc.callback.rts; + +/** + * 实时字幕回调具体消息 + * + * + */ +@lombok.Data +public class Data { + /** + * 字幕是否为完整的分句。 + * 。 + * 如果仅为了存储字幕,可只保存 definite=true 且 paragraph=true 的字幕,减少存储的数据量,并确保保存的字幕是完整的。 + */ + private boolean definite; + /** + * 字幕是否为完整的一句话。。 + */ + private boolean paragraph; + /** + * 字幕语言。 + */ + private String language; + /** + * 字幕序号。 + */ + private int sequence; + /** + * 字幕文本。 + */ + private String text; + /** + * 字幕来源者 ID。若字幕来源为真人用户,则该值为真人用户 UserId。若来源为智能体,则该值为智能体 UserId。 + */ + private String userId; + + /** + * 对话的轮次 ID。 + */ + private Integer roundId; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/rts/MessageParse.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/rts/MessageParse.java new file mode 100644 index 0000000..6295a07 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/rts/MessageParse.java @@ -0,0 +1,50 @@ +package com.sonic.cow.client.rtc.callback.rts; + +import com.alibaba.fastjson.JSON; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * 消息解析 + * + * @description: + * @author: mzc + * @date: 2025-08-27 15:13 + **/ +public class MessageParse { + + private static final String SUBTITLE_HEADER = "subv"; + + public static Subv unpack(String msg) throws Exception { + // Base64解码 + byte[] data = Base64.getDecoder().decode(msg); + + // 验证数据长度 + if (data.length < 8) { + throw new Exception("Data invalid"); + } + + // 验证头部 + String dataHeader = new String(data, 0, 4, StandardCharsets.UTF_8); + if (!SUBTITLE_HEADER.equals(dataHeader)) { + throw new Exception("Header not match"); + } + + // 验证数据大小 + int dataSize = ((data[4] & 0xFF) << 24) | + ((data[5] & 0xFF) << 16) | + ((data[6] & 0xFF) << 8) | + (data[7] & 0xFF); + + if (dataSize + 8 != data.length) { + throw new Exception("Size not match"); + } + + // 提取有效数据并反序列化 + byte[] subData = new byte[dataSize]; + System.arraycopy(data, 8, subData, 0, dataSize); + + return JSON.parseObject(subData, Subv.class); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/rts/Subv.java b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/rts/Subv.java new file mode 100644 index 0000000..b6e84ff --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/client/rtc/callback/rts/Subv.java @@ -0,0 +1,14 @@ +package com.sonic.cow.client.rtc.callback.rts; +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-08-27 15:02 + **/ +@lombok.Data +public class Subv { + private String type; + private List data; +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/config/ArkServiceConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/config/ArkServiceConfig.java new file mode 100644 index 0000000..e269bba --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/config/ArkServiceConfig.java @@ -0,0 +1,32 @@ +package com.sonic.cow.config; + +import com.byteplus.ark.runtime.service.ArkService; +import okhttp3.ConnectionPool; +import okhttp3.Dispatcher; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +@Configuration +public class ArkServiceConfig { + + @Value("${volcengine.baseUrl}") + private String baseUrl; + @Value("${volcengine.apiKey}") + private String apiKey; + + @Bean(name = "volcengineArkService") + public ArkService volcengineArkService() { + Dispatcher dispatcher = new Dispatcher(); + ConnectionPool connectionPool = new ConnectionPool(5, 1, TimeUnit.SECONDS); + return ArkService.builder() + .baseUrl(baseUrl) + .dispatcher(dispatcher) + .connectionPool(connectionPool) + .apiKey(apiKey) + .build(); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/config/Config.java b/sonic-cow/server/src/main/java/com/sonic/cow/config/Config.java new file mode 100644 index 0000000..61685e9 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/config/Config.java @@ -0,0 +1,48 @@ +package com.sonic.cow.config; + +import com.sonic.cow.common.GlobalConfig; +import com.sonic.cow.common.MybatisPlusConfig; +import com.sonic.common.AppRuntime; +import com.sonic.common.config.DefaultWebMvcConfig; +import com.sonic.common.exception.AbstractExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; + +import java.util.function.Consumer; + +/** + * Server服务唯一的配置入口 + * TODO: Import的各种Config,按需使用(例如,如果不需要mysql,kafka消息,redis等,可以去掉对应的Config) + * + * @author chenjun + */ +@Slf4j +@Configuration +@Import({GlobalConfig.class, DefaultWebMvcConfig.class, MybatisPlusConfig.class, RedisConfig.class, + EventConfig.class, SwaggerConfig.class}) +public class Config { + + /** + * XXX: ExceptionHandlerConfig 应该在 DefaultWebMvcConfig 前被初始. 否则 DefaultWebMvcConfig 的 exceptionHandlerHook 会生效 + */ + static class ExceptionHandlerConfig { + @Bean + @Profile({"!unittest && !local"}) + AbstractExceptionHandler.ExceptionHandlerHook exceptionHandlerHook(AppRuntime appRuntime) { + return new AbstractExceptionHandler.ExceptionHandlerHook() { + @Override + public String getAppId() { + return appRuntime.getAppId(); + } + + @Override + public Consumer getExceptionConsumer() { + return context -> log.error("未捕获内部异常, logText: {}", context.dumpRequest(), context.getException()); + } + }; + } + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/config/EventConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/config/EventConfig.java new file mode 100644 index 0000000..d6a8443 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/config/EventConfig.java @@ -0,0 +1,348 @@ +package com.sonic.cow.config; + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.rabbitmq.client.Channel; +import com.sonic.common.AppRuntime; +import com.sonic.common.event.*; +import com.sonic.common.utils.LogUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.annotation.RabbitListeners; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Payload; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * @author chenjun + */ +@Slf4j +public class EventConfig { + + /** TODO: 定义 Event.BuildInScene */ + public final static String DEFAULT_SCENE = Event.BuildInScene.SONIC.getCode(); + /** TODO: DEFAULT_SCENE + "_" + appName */ + public final static String DEFAULT_MODULE = DEFAULT_SCENE + "_" + "sonic"; + + /** TODO: DEFAULT_SCENE + "_" + appName */ + public final static String PIGEON = DEFAULT_SCENE + "_" + "pigeon"; + + /** TODO: DEFAULT_SCENE + "_" + appName */ + public final static String FROG = DEFAULT_SCENE + "_" + "frog"; + + @Value("${mq.exchange}") + private String mqExchange; + @Value("${mq.default.queue}") + private String defaultQueue; + @Value("${mq.default.routing-key}") + private String defaultRoutingKey; + + @Value("${mq.ai-text-gen-image.queue}") + private String aiTextGenImageQueue; + @Value("${mq.ai-text-gen-image.routing-key}") + private String aiTextGenImageRoutingKey; + + @Value("${mq.ai-image-gen-image.queue}") + private String aiImageGenImageQueue; + @Value("${mq.ai-text-gen-image.routing-key}") + private String aiImageGenImageRoutingKey; + + @Value("${mq.aiChat.queue}") + private String aiChatQueue; + @Value("${mq.aiChat.routing-key}") + private String aiChatRoutingKey; + + @Value("${mq.calc-heartbeat-level.queue}") + private String calcHeartbeatLevelQueue; + @Value("${mq.calc-heartbeat-level.routing-key}") + private String calcHeartbeatLevelRoutingKey; + + @Value("${mq.emotion-score.queue}") + private String emotionScoreQueue; + @Value("${mq.emotion-score.routing-key}") + private String emotionScoreRoutingKey; + + @Value("${mq.user-deduction-stat.queue}") + private String userDeductionStatQueue; + @Value("${mq.user-deduction-stat.routing-key}") + private String userDeductionStatRoutingKey; + + @Value("${mq.voice-call-deduction-dead.queue}") + public String voiceCallDeductionDeadQueue; + @Value("${mq.voice-call-deduction-dead.routing-key}") + public String voiceCallDeductionDeadRoutingKey; + + @Value("${mq.voice-call-deduction.queue}") + public String voiceCallDeductionQueue; + @Value("${mq.voice-call-deduction.routing-key}") + public String voiceCallDeductionRoutingKey; + + @Value("${mq.voice-call-webhook.queue}") + public String voiceCallWebhookQueue; + @Value("${mq.voice-call-webhook.routing-key}") + public String voiceCallWebhookRoutingKey; + + @Value("${mq.user-balance-insufficient-checkout.queue}") + private String userBalanceInsufficientCheckoutQueue; + @Value("${mq.user-balance-insufficient-checkout.routing-key}") + private String userBalanceInsufficientCheckoutRoutingKey; + + @Configuration + @Profile("!unittest") + static class Listener { + @Autowired + EventConsumer eventConsumer; + + @RabbitListeners({ + @RabbitListener(queues = {"${mq.default.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.ai-text-gen-image.queue}"}, concurrency = "6"), + @RabbitListener(queues = {"${mq.ai-image-gen-image.queue}"}, concurrency = "3"), + @RabbitListener(queues = {"${mq.aiChat.queue}"}, concurrency = "5"), + @RabbitListener(queues = {"${mq.emotion-score.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.voice-call-deduction.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.voice-call-webhook.queue}"}, concurrency = "2"), + }) + public void process(@Payload Message message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) { + try { + LogUtils.setTraceId(); + byte[] msgBody = message.getBody(); + String contentEncoding = message.getMessageProperties().getContentEncoding(); + String bodyStr = new String(msgBody, Charset.forName(ObjectUtils.defaultIfNull(contentEncoding, "UTF-8"))); + MessageProperties pro = message.getMessageProperties(); + if (log.isDebugEnabled()) { + log.info("receive msg routingKey:{}->consumerQueue:{}->redelivered:{}->body:{}", + pro.getReceivedRoutingKey(), pro.getConsumerQueue(), pro.getRedelivered(), bodyStr); + } + eventConsumer.onEvent(bodyStr, ImmutableMap.of("record", JSON.toJSONString(message))); + + //消息确认,multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息 + channel.basicAck(deliveryTag, false); + } catch (Exception e) { + log.error("===> mq handler error, message: {}", JSON.toJSONString(message), e); + //消息拒绝,//requeue=true,表示将消息重新放入到队列中,false:表示直接从队列中删除,此时和basicAck(long deliveryTag, false)的效果一样 + try { + channel.basicReject(deliveryTag,true); + } catch (IOException ioException) { + log.error("channel.basicReject error. message: {}, deliveryTag: {}", JSON.toJSONString(message), deliveryTag, ioException); + } + } finally { + LogUtils.removeTraceId(); + } + } + + } + + @Bean + EventProducer eventProducer(AppRuntime appRuntime, RabbitTemplate rabbitTemplate, TaskExecutor taskExecutor) { + BiConsumer callback = (event, meta) -> {}; + return new RabbitmqEventProducer(rabbitTemplate, DEFAULT_MODULE, DEFAULT_SCENE, appRuntime.getAppId(), + RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, defaultRoutingKey), taskExecutor, callback); + } + + @Bean + EventConsumer eventConsumer(AppRuntime appRuntime, EventHandlerRepository eventHandlerRepository) { + Consumer callback = (eventWrapper) -> {}; + return new DefaultEventConsumer(appRuntime.getAppName(), eventHandlerRepository, callback); + } + + @Bean + EventHandlerRepository eventHandlerRepository() { + return new EventHandlerRepository((ex, logText) -> log.error("MQ,handle error {}", logText, ex)); + } + + @Bean + public DirectExchange messageServerExchange(){ + return new DirectExchange(mqExchange); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta aiTextGenImageMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, aiTextGenImageRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta aiImageGenImageMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, aiImageGenImageRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta aiChatMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, aiChatRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta calcHeartbeatLevelMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, calcHeartbeatLevelRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta emotionScoreMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, emotionScoreRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta userDeductionStatMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, userDeductionStatRoutingKey); + } + + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta voiceCallDeductionDeadMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, voiceCallDeductionDeadRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta voiceCallDeductionMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, voiceCallDeductionRoutingKey); + } + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta voiceCallWebhookMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, voiceCallWebhookRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta userBalanceInsufficientCheckoutMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, userBalanceInsufficientCheckoutRoutingKey); + } + + + + @Bean + public Binding bindingDefaultQueueExchange(DirectExchange exchange, Queue defaultQueue) { + return bindingExchange(exchange, defaultQueue, defaultRoutingKey); + } + + + @Bean + public Binding bindingAiTextGenImageQueueExchange(DirectExchange exchange, Queue aiTextGenImageQueue) { + return bindingExchange(exchange, aiTextGenImageQueue, aiTextGenImageRoutingKey); + } + + @Bean + public Binding bindingAiImageGenImageQueueExchange(DirectExchange exchange, Queue aiTextGenImageQueue) { + return bindingExchange(exchange, aiTextGenImageQueue, aiImageGenImageRoutingKey); + } + + @Bean + public Binding bindingAiChatQueueExchange(DirectExchange exchange) { + return bindingExchange(exchange, aiChatQueue(), aiChatRoutingKey); + } + + @Bean + public Binding bindingCalcHeartbeatLevelQueueExchange(DirectExchange exchange, Queue calcHeartbeatLevelQueue) { + return bindingExchange(exchange, calcHeartbeatLevelQueue, calcHeartbeatLevelRoutingKey); + } + @Bean + public Binding bindingEmotionScoreQueueExchange(DirectExchange exchange, Queue emotionScoreQueue) { + return bindingExchange(exchange, emotionScoreQueue, emotionScoreRoutingKey); + } + + @Bean + public Binding bindingUserDeductionStatQueueExchange(DirectExchange exchange, Queue userDeductionStatQueue) { + return bindingExchange(exchange, userDeductionStatQueue, userDeductionStatRoutingKey); + } + + @Bean + public Binding bindingVoiceCallDeductionDeadQueueExchange(DirectExchange exchange) { + return bindingExchange(exchange, voiceCallDeductionDeadQueue(), voiceCallDeductionDeadRoutingKey); + } + + + @Bean + public Binding bindingVoiceCallDeductionQueueExchange(DirectExchange exchange) { + return bindingExchange(exchange, voiceCallDeductionQueue(), voiceCallDeductionRoutingKey); + } + + @Bean + public Binding bindingVoiceCallWebhookQueueExchange(DirectExchange exchange) { + return bindingExchange(exchange, voiceCallWebhookQueue(), voiceCallWebhookRoutingKey); + } + + @Bean + public Binding bindingUserBalanceInsufficientCheckoutQueueExchange(DirectExchange exchange, Queue userBalanceInsufficientCheckoutQueue) { + return bindingExchange(exchange, userBalanceInsufficientCheckoutQueue, userBalanceInsufficientCheckoutRoutingKey); + } + + + + @Bean + public Queue defaultQueue() { + return new Queue(defaultQueue,true); + } + + @Bean + public Queue aiTextGenImageQueue() { + return new Queue(aiTextGenImageQueue,true); + } + + @Bean + public Queue aiImageGenImageQueue() { + return new Queue(aiImageGenImageQueue,true); + } + + @Bean + public Queue aiChatQueue() { + return new Queue(aiChatQueue,true); + } + + @Bean + public Queue calcHeartbeatLevelQueue() { + return new Queue(calcHeartbeatLevelQueue,true); + } + + @Bean + public Queue emotionScoreQueue() { + return new Queue(emotionScoreQueue,true); + } + + @Bean + public Queue userDeductionStatQueue() { + return new Queue(userDeductionStatQueue,true); + } + + @Bean + public Queue voiceCallDeductionDeadQueue() { + Map map = Maps.newHashMap(); + //死信对列过期后转发的交换机 + map.put("x-dead-letter-exchange", mqExchange); + //死信对列过期后转发后的路由键 + map.put("x-dead-letter-routing-key", voiceCallDeductionRoutingKey); + return new Queue(voiceCallDeductionDeadQueue, true, false, false, map); + } + + @Bean + public Queue voiceCallDeductionQueue() { + return new Queue(voiceCallDeductionQueue, true); + } + @Bean + public Queue voiceCallWebhookQueue() { + return new Queue(voiceCallWebhookQueue, true); + } + + @Bean + public Queue userBalanceInsufficientCheckoutQueue() { + return new Queue(userBalanceInsufficientCheckoutQueue,true); + } + + public Binding bindingExchange(DirectExchange exchange, Queue queueMessage, String routingKey) { + return BindingBuilder.bind(queueMessage).to(exchange).with(routingKey); + } + +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/config/IVisualServiceConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/config/IVisualServiceConfig.java new file mode 100644 index 0000000..9b15ef7 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/config/IVisualServiceConfig.java @@ -0,0 +1,26 @@ +package com.sonic.cow.config; + +import com.volcengine.service.visual.IVisualService; +import com.volcengine.service.visual.impl.VisualServiceImpl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class IVisualServiceConfig { + + @Value("${volcengine.cnAccessKey}") + private String accessKey; + + @Value("${volcengine.cnSecretKey}") + private String secretKey; + + @Bean + public IVisualService visualService() { + IVisualService visualService = VisualServiceImpl.getInstance(); + visualService.setAccessKey(accessKey); + visualService.setSecretKey(secretKey); + return visualService; + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/config/RedisConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/config/RedisConfig.java new file mode 100644 index 0000000..dcc93e2 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/config/RedisConfig.java @@ -0,0 +1,39 @@ +package com.sonic.cow.config; + +import com.sonic.common.utils.RedisLock; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * 有关Redis的配置 + * + * TODO: 如果不需要Redis, 可以删除该文件并且在{@link Config}类@Import的注解中移除对RedisConfig的引用 + */ +@Slf4j +public class RedisConfig { + + /** + * redisWrapper用于分布式锁RedisLock + * + * @param redisTemplate + * @return + */ + @Bean + RedisLock.RedisWrapper redisWrapper(RedisTemplate redisTemplate) { + return new RedisLock.RedisWrapper() { + @Override + public void delete(String key) { + redisTemplate.delete(key); + } + + @Override + public boolean lock(String key, String value, long expireMills) { + return redisTemplate.opsForValue().setIfAbsent(key, value, expireMills, TimeUnit.MILLISECONDS); + } + }; + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/config/RedissonConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/config/RedissonConfig.java new file mode 100644 index 0000000..e2bd29f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/config/RedissonConfig.java @@ -0,0 +1,52 @@ +package com.sonic.cow.config; + +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + + /** + * 配置 RedissonClient,支持单机和集群模式 + */ + @Bean + public RedissonClient redissonClient(RedisProperties redisProperties) { + Config config = new Config(); + + // 设置 Netty 线程数,使用默认值 0(Redisson 默认使用 CPU 核心数的两倍) + config.setNettyThreads(0); + + // 判断是否配置了集群节点 + if (redisProperties.getCluster() != null && redisProperties.getCluster().getNodes() != null + && !redisProperties.getCluster().getNodes().isEmpty()) { + // 集群模式配置 + config.useClusterServers() + .setScanInterval(2000) // 集群状态扫描间隔,单位毫秒 + .addNodeAddress(redisProperties.getCluster().getNodes().stream() + .map(node -> "redis://" + node) + .toArray(String[]::new)) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null); + } else { + // 单机模式配置 + config.useSingleServer() + .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort()) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null) + .setDatabase(redisProperties.getDatabase()); + } + + // 如果启用了 SSL,设置 SSL 相关配置 + if (redisProperties.isSsl()) { + config.useSingleServer().setSslEnableEndpointIdentification(false); // 禁用主机名验证 + // 集群模式下,Redisson 会自动应用 SSL 配置 + } + + return Redisson.create(config); + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/config/RestTemplateConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/config/RestTemplateConfig.java new file mode 100644 index 0000000..c08694e --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/config/RestTemplateConfig.java @@ -0,0 +1,24 @@ +package com.sonic.cow.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(ClientHttpRequestFactory factory){ + return new RestTemplate(factory); + } + + @Bean + public ClientHttpRequestFactory simpleClientHttpRequestFactory(){ + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setReadTimeout(5000);//单位为ms + factory.setConnectTimeout(5000);//单位为ms + return factory; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/config/ResultCode.java b/sonic-cow/server/src/main/java/com/sonic/cow/config/ResultCode.java new file mode 100644 index 0000000..c7cad42 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/config/ResultCode.java @@ -0,0 +1,43 @@ +package com.sonic.cow.config; + +import com.sonic.common.rpc.ApiResultCode; +import lombok.Getter; + +/** + * @author chenjun + */ +@Getter +public enum ResultCode implements ApiResultCode { + + /** + * TODO: 可以在此处扩展服务自身需要用到的错误码信息 + */ + BIZ_ERROR1("0000", "业务异常1"), + DEMO_CREATED_FAIL("0001", "新增Demo实体失败"); + + private final String errorCode; + private final String errorMsg; + + ResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/config/SsoConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/config/SsoConfig.java new file mode 100644 index 0000000..62c619b --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/config/SsoConfig.java @@ -0,0 +1,16 @@ +package com.sonic.cow.config; + +import com.sonic.common.auth.GateWaySessionInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SsoConfig { + + + @Bean + public GateWaySessionInterceptor gateWaySessionInterceptor() { + return new GateWaySessionInterceptor(); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/config/SwaggerConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/config/SwaggerConfig.java new file mode 100644 index 0000000..7782f67 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/config/SwaggerConfig.java @@ -0,0 +1,110 @@ +package com.sonic.cow.config; + + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import springfox.documentation.RequestHandler; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; + +/** + * swagger配置 + * @author code + */ +@Slf4j +@EnableSwagger2 +public class SwaggerConfig { + + private static final String SPLIT = ","; + + @Value("${swagger.enabled:false}") + private Boolean swaggerEnabled; + + @Value("${swagger.base.package}") + private String basePackageValue; + + private final String DEFAULT_BASE_PACKAGE = "com.sonic.bs.controller"; + + + public static Predicate basePackage(final String basePackage) { + return input -> declaringClass(input).transform(handlerPackage(basePackage)).or(true); + } + + private static Optional> declaringClass(RequestHandler input) { + return Optional.fromNullable(input.declaringClass()); + } + + private static Function, Boolean> handlerPackage(final String basePackage) { + return input -> { + // 循环判断匹配 + for (String strPackage : basePackage.split(SPLIT)) { + boolean isMatch = input.getPackage().getName().startsWith(strPackage); + if (isMatch) { + return true; + } + } + return false; + }; + } + + @Bean + public Docket createRestApi() { + basePackageValue = null == basePackageValue || ("").equals(basePackageValue) ? DEFAULT_BASE_PACKAGE : basePackageValue; + log.info("===> swagger base package : ", basePackageValue); + + //在配置好的配置类中增加此段代码即可 + ParameterBuilder ticketPar = new ParameterBuilder(); + List pars = new ArrayList(); + ticketPar.name("_tk_") + //name表示名称,description表示描述 + .description("token") + .modelRef(new ModelRef("string")) + .parameterType("header") + .required(false) + .defaultValue("") + .build();//required表示是否必填,defaultvalue表示默认值 + + //添加完此处一定要把下边的带***的也加上否则不生效 + pars.add(ticketPar.build()); + + + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .enable(swaggerEnabled) + .select() + .apis(basePackage(basePackageValue)) + .paths(PathSelectors.any()) + .build() + //************把消息头添加 + .globalOperationParameters(pars); + } + + /** + * TODO: 更改文案配置 + * @return + */ + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("epal") + .description("epal API") + .version("1.0") + .contact(new Contact("epal", "", "admin.epal.gg")) + .build(); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/ContentApi.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/ContentApi.java new file mode 100644 index 0000000..461c607 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/ContentApi.java @@ -0,0 +1,47 @@ +package com.sonic.cow.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.cow.domain.input.AiCreateGenSupContentInput; +import com.sonic.cow.domain.input.GenAiUserContentV1Input; +import com.sonic.cow.service.GenAiUserContentService; +import com.sonic.cow.service.GenSupContentService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 内容接口 + * + * @author code + */ +@RestController +@Slf4j +public class ContentApi { + + @Autowired + private GenSupContentService genSupContentService; + @Autowired + private GenAiUserContentService genAiUserContentService; + + @IgnoreAuth + @ApiOperation(value = "文本生成语音", tags = {"API-接口"}) + @PostMapping("/api/gen/sup-content") + public Result> genSupContent(@RequestBody AiCreateGenSupContentInput input) { + return Result.success(genSupContentService.aiCreateGenSupContent(input.getUserId(), input.getAiId())); + } + + @IgnoreAuth + @ApiOperation(value = "一键生成", tags = {"API-接口"}) + @PostMapping("/api/gen/user-content") + public Result genUserContent(@RequestBody GenAiUserContentV1Input input) throws Exception { + return Result.success(genAiUserContentService.genAiUserContentV1(-1L, input).getContent()); + } + +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/ImageApi.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/ImageApi.java new file mode 100644 index 0000000..90f0c2a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/ImageApi.java @@ -0,0 +1,35 @@ +package com.sonic.cow.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.cow.domain.input.CheckImageIsAIGeneratedInput; +import com.sonic.cow.service.GenImageTaskRecordService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * AI接口 + * + * @author code + */ +@RestController +@Slf4j +public class ImageApi { + + @Autowired + private GenImageTaskRecordService genImageTaskRecordService; + + @IgnoreAuth + @ApiOperation(value = "添加相册图片,背景图片,ai形象图时,检查图片是否是AI生成的", tags = {"API-接口"}) + @PostMapping("/api/checkImageIsAIGenerated") + public Result checkImageIsAIGenerated(@RequestBody CheckImageIsAIGeneratedInput input) { + genImageTaskRecordService.checkImageIsAIGenerated(input.getUserId(), input.getImgUrlList()); + return Result.success(); + } + +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/NsfwCheckApi.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/NsfwCheckApi.java new file mode 100644 index 0000000..704fa70 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/NsfwCheckApi.java @@ -0,0 +1,35 @@ +package com.sonic.cow.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.cow.client.ContextChatClient; +import com.sonic.cow.lib.input.NsfwCheckInput; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * AI接口 + * + * @author code + */ +@RestController +@Slf4j +public class NsfwCheckApi { + + @Autowired + private ContextChatClient contextChatClient; + + @IgnoreAuth + @ApiOperation(value = "敏感词校验接口", tags = {"API-接口"}) + @PostMapping("/api/nsfw/check") + public Result nsfwCheck(@RequestBody NsfwCheckInput input) { + contextChatClient.nsfwCheck(input.getContent()); + return Result.success(); + } + +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/VoiceApi.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/VoiceApi.java new file mode 100644 index 0000000..909d292 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/api/VoiceApi.java @@ -0,0 +1,36 @@ +package com.sonic.cow.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.cow.domain.input.CheckImageIsAIGeneratedInput; +import com.sonic.cow.domain.input.VoiceTtsV2Input; +import com.sonic.cow.service.GenImageTaskRecordService; +import com.sonic.cow.service.VoiceService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * 语音接口 + * + * @author code + */ +@RestController +@Slf4j +public class VoiceApi { + + @Autowired + private VoiceService voiceService; + + @IgnoreAuth + @ApiOperation(value = "文本生成语音", tags = {"API-接口"}) + @PostMapping("/api/tts") + public Result tts(@RequestBody VoiceTtsV2Input input) { + return Result.success(voiceService.tts(input.getUserId(), input)); + } + +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/mock/MockController.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/mock/MockController.java new file mode 100644 index 0000000..56533cc --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/mock/MockController.java @@ -0,0 +1,303 @@ +package com.sonic.cow.controller.mock; + + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessage; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.sonic.common.rpc.Result; +import com.sonic.cow.client.ContextChatClient; +import com.sonic.cow.client.DeepSeekClient; +import com.sonic.cow.client.ImageGenImageClient; +import com.sonic.cow.client.ResponseChatClient; +import com.sonic.cow.client.request.ContentRequest; +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.client.response.ResponseChatResponse; +import com.sonic.cow.domain.bo.AiGen4Bo; +import com.sonic.cow.domain.bo.TextGenImagePromptParse; +import com.sonic.cow.domain.bo.TextPromptParse; +import com.sonic.cow.domain.entity.PromptConfig; +import com.sonic.cow.domain.input.AiGenMockInput; +import com.sonic.cow.domain.input.GenAiUserContentV1Input; +import com.sonic.cow.domain.input.GenSupContentInput; +import com.sonic.cow.domain.input.MockInput; +import com.sonic.cow.enums.PromptTypeEnum; +import com.sonic.cow.event.outer.payload.AiChatPayload; +import com.sonic.cow.service.*; +import com.sonic.cow.service.impl.GenAiUserContentServiceImpl; +import com.sonic.frog.lib.client.AiClient; +import com.sonic.frog.lib.client.AiUserAlbumClient; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; +import com.sonic.frog.lib.output.AiInfoApiOutput; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.input.SendAiTextMessageInput; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @Author code + * @Description TODO + * @Date 2024/1/8 11:06 + * @Version 1.0 + */ +@RestController +@Slf4j +public class MockController { + + @Autowired + private GenAiUserContentService genAiUserContentService; + @Autowired + private ImageGenImageClient imageGenImageClient; + @Autowired + private GenSupContentService genSupContentService; + @Autowired + private ContextSessionCacheService contextSessionCacheService; + @Autowired + private DeepSeekClient deepSeekClient; + @Autowired + private ContextChatClient contextChatClient; + @Autowired + private InputRequestBuildService inputRequestBuildService; + @Autowired + private PromptConfigService promptConfigService; + @Autowired + private ResponseChatClient responseChatClient; + @Autowired + private ChatHandlerService chatHandlerService; + @Autowired + private AiUserAlbumClient aiUserAlbumClient; + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private AiClient aiClient; + + + @IgnoreAuth + @PostMapping(value = "/mock/textGenImage6Prompt") + @ApiOperation(value = "文生图-生成6组不同promp测试", tags = {"mock"}) + public Result textGenImage6Prompt() { + String promptType = PromptTypeEnum.TEXT_TO_IMAGE_PROMPT.name(); + HashMap map = Maps.newHashMap(); + map.put("content", "花园中漫步的漂亮女孩"); + map.put("imageStylePrompt", "manga style, clean black outlines, expressive face, large eyes, halftone shading, cell shading,black and white,"); + map.put("commonImagePrompt", "caucasian features, western facial structure, proportional anatomy, highly detailed, ultra high resolution, 8k, masterpiece, professional studio lighting, cinematic lighting, volumetric shadows, master piece"); + TextGenImagePromptParse textGenImagePromptParse = null; + try { + textGenImagePromptParse = genAiUserContentService.genContent(promptType, map, TextGenImagePromptParse.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + return Result.success(textGenImagePromptParse); + } + + @IgnoreAuth + @PostMapping(value = "/mock/imageGenImage") + @ApiOperation(value = "图生图-测试", tags = {"mock"}) + public Result imageGenImageMock() { + String imageUrl = "https://b0.bdstatic.com/ugc/aBlDFCWKxpvg5MW1yQHnAA1bb451086e5ebe96d8bc3c1e63c91211.jpg"; + String imageBase64 = null; + try { + imageBase64 = imageGenImageClient.imageGenImage(imageUrl, "大学生模样,走在校园里"); + } catch (Exception e) { + throw new RuntimeException(e); + } + log.info("imageGenImageMock imageBase64:{}", imageBase64); + return Result.success(imageBase64); + } + + @IgnoreAuth + @ApiOperation(value = "生成辅助对话内容", tags = {"WEB-AI一键生成"}) + @RequestMapping(value = "/mock/gen/sup-content", produces = {"application/json"}, method = RequestMethod.POST) + public Result> genSupContent(@Validated @RequestBody GenSupContentInput input) { + return Result.success(genSupContentService.genSupContent(438933526413313L, input)); + } + + @IgnoreAuth + @ApiOperation(value = "生成辅助对话内容", tags = {"WEB-AI一键生成"}) + @RequestMapping(value = "/mock/gen/sup-content-v2", produces = {"application/json"}, method = RequestMethod.POST) + public Result> aiGenSupContentV2(@Validated @RequestBody GenSupContentInput input) { + return Result.success(genSupContentService.genSupContentV2(438933526413313L, input.getAiId(), null)); + } + + @IgnoreAuth + @ApiOperation(value = "删除redis缓存", tags = {"WEB-AI一键生成"}) + @RequestMapping(value = "/mock/response/redis-del", produces = {"application/json"}, method = RequestMethod.POST) + public Result redisDelResponse(@Validated @RequestBody MockInput input) { + contextSessionCacheService.deleteRedis(input.getUserId(), input.getAiId()); + return Result.success(); + } + + @IgnoreAuth + @PostMapping(value = "/mock/chat-score") + @ApiOperation(value = "根据聊天记录进行情绪价值打分", tags = {"mock"}) + public Result chatScoreMock() { + String promptType = PromptTypeEnum.CHAT_SCORE_SYSTEM_PROMPT_TEMPLATE.name(); + List messages = Lists.newArrayList(); + messages.add(ChatMessage.builder() + .role(ChatMessageRole.ASSISTANT) + .content("我喜欢你") + .build()); + messages.add(ChatMessage.builder() + .role(ChatMessageRole.ASSISTANT) + .content("嘻嘻,我也喜欢你") + .build()); + HashMap map = Maps.newHashMap(); + map.put("content", JSON.toJSONString(messages)); + TextPromptParse textPromptParse = null; + try { + textPromptParse = genAiUserContentService.genContent(promptType, map, TextPromptParse.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + return Result.success(textPromptParse); + } + + + @IgnoreAuth + @PostMapping(value = "/mock/doubao/aiGen") + @ApiOperation(value = "使用doubao模型来测试一键生成", tags = {"mock"}) + public Result doubaoAiGen(@RequestBody AiGenMockInput input) { + return Result.success(contextChatClient.aiGen(input.getSystemPrompt(), input.getTemperatur())); + } + + @IgnoreAuth + @PostMapping(value = "/mock/deepSeek/aiGen") + @ApiOperation(value = "使用deepSeek模型来测试一键生成", tags = {"mock"}) + public Result deepSeekAiGen(@RequestBody AiGenMockInput input) { + return Result.success(deepSeekClient.aiGen(input.getSystemPrompt(), input.getTemperatur())); + } + + + @IgnoreAuth + @PostMapping(value = "/mock/buildSystemPromptContent") + @ApiOperation(value = "构造系统提示词", tags = {"mock"}) + public Result buildSystemPromptContent(@RequestBody AiGenMockInput input) { + AiInfoApiOutput aiUserInfo = aiClient.getAiInfo(input.getAiId()); + return Result.success(inputRequestBuildService.buildSystemPromptContent(input.getFromUserId(), input.getAiId(), aiUserInfo)); + } + + @IgnoreAuth + @PostMapping(value = "/mock/buildVoiceChatSystemPromptContent") + @ApiOperation(value = "构造语音系统提示词", tags = {"mock"}) + public Result buildVoiceChatSystemPromptContent(@RequestBody AiGenMockInput input) { + AiInfoApiOutput aiUserInfo = aiClient.getAiInfo(input.getAiId()); + return Result.success(inputRequestBuildService.buildVoiceChatSystemPromptContent(input.getFromUserId(), input.getAiId(), aiUserInfo)); + } + + @IgnoreAuth + @PostMapping(value = "/mock/autoChat") + @ApiOperation(value = "24小时自动发起聊天内容", tags = {"mock"}) + public Result autoChat(@RequestBody AiGenMockInput input) { + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.AUTO_SEND_CHAT_SYSTEM_PROMPT_TEMPLATE.name()); + Long fromUserId = input.getFromUserId(); + Long toUserId = input.getAiId(); + //获取上下文缓存 这个缓存一定是存在的 + String responseId = contextSessionCacheService.getByRedis(fromUserId, toUserId); + log.info("===> autoChat responseId : {}", responseId); + //构造系统提示词 + List inputRequests = Lists.newArrayList(InputRequest.builder() + .role(ChatMessageRole.SYSTEM) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(systemPromptConfig.getPromptTemplate()).build())).build()); + //执行聊天 不将添加缓存 + ResponseChatResponse chatResponse = responseChatClient.responseStructuredChat(responseId, null, inputRequests, null, false); + // 发送消息给用户 + SendAiTextMessageInput sendAiTextMessageInput = new SendAiTextMessageInput(); + sendAiTextMessageInput.setFromUserId(toUserId); + sendAiTextMessageInput.setToUserId(fromUserId); + sendAiTextMessageInput.setContent(chatResponse.getMessage()); + imMessageClient.sendAiToUserTextMessage(sendAiTextMessageInput); + //发送IM消息 + log.info("===> chatResponse : {}", chatResponse); + return Result.success(chatResponse); + } + + @IgnoreAuth + @PostMapping(value = "/mock/genStartVoiceChatDialoguePrologue") + @ApiOperation(value = "生成语音通话开场白", tags = {"mock"}) + public Result genStartVoiceChatDialoguePrologue(@RequestBody AiGenMockInput input) { + Long fromUserId = input.getFromUserId(); + Long toUserId = input.getAiId(); + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.START_VOICE_CHAT_DIALOGUE_PROLOGUE.name()); + //获取上下文缓存 这个缓存一定是存在的 + String responseId = contextSessionCacheService.getByRedis(fromUserId, toUserId); + List inputRequests = Lists.newArrayList(); + if (StringUtils.isEmpty(responseId)) { + AiChatPayload payload = AiChatPayload.builder().fromUserId(fromUserId).toUserId(toUserId).build(); + //调用公共方法,组装入参数据 + inputRequestBuildService.buildV2(payload, inputRequests, 50,true); + } + //构造系统提示词 + inputRequests.add(InputRequest.builder() + .role(ChatMessageRole.SYSTEM) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(systemPromptConfig.getPromptTemplate()).build())).build()); + //执行聊天 不将添加缓存 + ResponseChatResponse chatResponse = responseChatClient.responseStructuredChat(responseId, null, inputRequests, null, false); + log.info("===> genStartVoiceChatDialoguePrologue chatResponse : {}", chatResponse); + return Result.success(chatResponse); + } + + + @IgnoreAuth + @PostMapping(value = "/mock/chat-send-album") + @ApiOperation(value = "生成语音通话开场白", tags = {"mock"}) + public Result sendAlbum(@RequestBody AiGenMockInput input) { + AIUserAlbumApiOutput album = aiUserAlbumClient.getRandomLockImage(input.getUserId(), input.getAiId()); + AiChatPayload payload = AiChatPayload.builder().fromUserId(input.getFromUserId()).toUserId(input.getAiId()).build(); + ResponseChatResponse chatResponse = ResponseChatResponse.builder().message("自定义图片消息").score(new BigDecimal(0.1)).build(); + chatHandlerService.sendImCustomMessage(payload, "自定义图片消息", album, chatResponse); + return Result.success(); + } + + @IgnoreAuth + @PostMapping(value = "/mock/set6PromptTemplate") + @ApiOperation(value = "配置生成6组不同的prompt的模板", tags = {"mock"}) + public Result set6PromptTemplate(@RequestBody AiGenMockInput input) { + PromptConfig promptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.TEXT_TO_IMAGE_PROMPT.name()); + promptConfig.setPromptTemplate(input.getSystemPrompt()); + promptConfig.setTemperature(input.getTemperatur()); + promptConfigService.updateById(promptConfig); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "提取json内容", tags = {"WEB-AI一键生成"}) + @RequestMapping(value = "/mock/ext/json", produces = {"application/json"}, method = RequestMethod.POST) + public Result extJson(@Validated @RequestBody GenAiUserContentV1Input input) throws Exception { + PromptConfig promptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.EXTRACT_JSON_CONTENT.name()); + //系统提示词替换(根据参数名替换) + Map data = JSONObject.parseObject(JSONObject.toJSONString(input), Map.class); + String formatted = GenAiUserContentServiceImpl.processTemplate(promptConfig.getPromptTemplate(), data); + AiGen4Bo bo = contextChatClient.aiGen4(formatted, promptConfig.getModel(), promptConfig.getTemperature(), promptConfig.getTopp()); + return Result.success(bo); + } + + + @IgnoreAuth + @ApiOperation(value = "聊天提示词", tags = {"WEB-聊天"}) + @RequestMapping(value = "/mock/chat/prompt", produces = {"application/json"}, method = RequestMethod.POST) + public Result chatPrompt(@Validated @RequestBody AiChatPayload input) throws Exception { + List inputRequests = Lists.newArrayList(); + inputRequestBuildService.buildV2(input, inputRequests,100,true); + return Result.success(inputRequests); + } + + @IgnoreAuth + @ApiOperation(value = "一键生成", tags = {"API-接口"}) + @PostMapping("/mock/gen/user-content") + public Result genUserContent(@RequestBody GenAiUserContentV1Input input) throws Exception { + return Result.success(genAiUserContentService.genAiUserContentV1(-1L, input).getContent()); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/mock/VoiceMockController.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/mock/VoiceMockController.java new file mode 100644 index 0000000..a10fe78 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/mock/VoiceMockController.java @@ -0,0 +1,78 @@ +package com.sonic.cow.controller.mock; + +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.cow.domain.input.TtsDeAmountInput; +import com.sonic.cow.domain.input.VoiceAsrInput; +import com.sonic.cow.domain.input.VoiceTtsV2Input; +import com.sonic.cow.domain.output.AsrOutput; +import com.sonic.cow.enums.ToastResultCode; +import com.sonic.cow.limit.RequestLimit; +import com.sonic.cow.service.VoiceService; +import com.sonic.cow.service.VoiceV2Service; +import com.sonic.cow.utils.Base64Utils; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.util.UUID; + +/** + * Web-语音处理 + */ +@RestController +@Slf4j +public class VoiceMockController { + + @Autowired + private VoiceService voiceService; + @Autowired + private VoiceV2Service voiceV2Service; + + @IgnoreAuth + @RequestLimit(count = 100, time = 86400000) + @ApiOperation(value = "语音转文本V2版本", tags = {"Web-语音处理"}) + @PostMapping(value = "/mock/voice/asr-v2") + public Result asrV2(@RequestBody VoiceAsrInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) throws UnirestException, InterruptedException { + session.setUserId(session.getUserId() == null ? -1L : session.getUserId()); + return Result.success(voiceService.asr(session.getUserId(), input)); + } + + @IgnoreAuth + @RequestLimit(count = 100, time = 86400000) + @ApiOperation(value = "语音转文本V3版本", tags = {"Web-语音处理"}) + @PostMapping(value = "/mock/voice/asr-v3") + public Result asrV3(@RequestBody VoiceAsrInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) throws UnirestException { + //保存到临时 File + File tempFile = Base64Utils.decodeToTempFile(input.getData(), UUID.randomUUID() + ".mp3"); + return Result.success(voiceV2Service.asr(session.getUserId(), input.getAiId(), tempFile)); + } + + @IgnoreAuth + @RequestLimit(count = 1000, time = 86400000) + @ApiOperation(value = "生成语音V2版本", tags = {"Web-语音处理"}) + @PostMapping(value = "/mock/voice/tts-v2") + public Result ttsV2(@RequestBody VoiceTtsV2Input input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + session.setUserId(session.getUserId() == null ? -1L : session.getUserId()); + return Result.success(voiceService.tts(session.getUserId(), input)); + } + + @IgnoreAuth + @RequestLimit(count = 1000, time = 86400000) + @ApiOperation(value = "生成语音扣费", tags = {"Web-语音处理"}) + @PostMapping(value = "/mock/voice/tts-de-amount") + public Result ttsDeAmount(@RequestBody TtsDeAmountInput input, @ApiParam(hidden = true) HttpServletRequest request) { + //验证密钥是否正确 + ToastResultCode.SYS_SYSTEM_EXCEPTION.check(!input.getSk().equals("kILi*&u878u*(H778GB6FVYB0@#9jm79ygvbnj%$Rtb*&Gob")); + voiceService.ttsDeAmount(input.getUserId(), input.getAiId()); + return Result.success(); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/probe/ProbeController.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/probe/ProbeController.java new file mode 100644 index 0000000..ad9391f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/probe/ProbeController.java @@ -0,0 +1,23 @@ +package com.sonic.cow.controller.probe; + +import com.sonic.common.auth.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 探测接口 + */ +@RestController +@Slf4j +public class ProbeController { + + @IgnoreAuth + @ApiOperation(value = "检测服务存活状态", tags = {"探针"}) + @PostMapping("/probe/check") + public void probeCheck() { + + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/AIUserContentWeb.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/AIUserContentWeb.java new file mode 100644 index 0000000..c97db4e --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/AIUserContentWeb.java @@ -0,0 +1,56 @@ +package com.sonic.cow.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.cow.domain.input.GenAiUserContentV1Input; +import com.sonic.cow.domain.input.GenAutoChatContentInput; +import com.sonic.cow.domain.input.GenSupContentInput; +import com.sonic.cow.domain.output.GenAiUserContentV1Output; +import com.sonic.cow.domain.output.GenSupContentOutput; +import com.sonic.cow.enums.SexEnums; +import com.sonic.cow.service.GenAiUserContentService; +import com.sonic.cow.service.GenSupContentService; +import com.sonic.cow.utils.DateUtils; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * WEB-AI一键生成 + */ +@Slf4j +@RestController +public class AIUserContentWeb { + + @Autowired + private GenAiUserContentService genAiUserContentService; + @Autowired + private GenSupContentService genSupContentService; + + @ApiOperation(value = "生成人物的通用方法-V1版本", tags = {"WEB-AI一键生成"}) + @RequestMapping(value = "/web/gen/user-content-v1", produces = {"application/json"}, method = RequestMethod.POST) + public Result aiGenUserContentV1(@Validated @RequestBody GenAiUserContentV1Input input, @ApiParam(hidden = true) Session session) throws Exception { + return Result.success(genAiUserContentService.genAiUserContentV1(session.getUserId(), input)); + } + + @ApiOperation(value = "生成辅助对话内容", tags = {"WEB-AI一键生成"}) + @RequestMapping(value = "/web/gen/sup-content", produces = {"application/json"}, method = RequestMethod.POST) + public Result aiGenSupContent(@Validated @RequestBody GenSupContentInput input, @ApiParam(hidden = true) Session session) { + return Result.success(genSupContentService.genSupContent(session.getUserId(), input)); + } + + @ApiOperation(value = "用户超过3分钟停留没有发消息了生成自动聊天内容并发送IM消息", tags = {"WEB-AI一键生成"}) + @RequestMapping(value = "/web/g/at-ct", produces = {"application/json"}, method = RequestMethod.POST) + public Result genAutoChatAndSendImMessage(@Validated @RequestBody GenAutoChatContentInput input, @ApiParam(hidden = true) Session session) { + genSupContentService.genAutoChatAndSendImMessage(session.getUserId(), input.getAiId()); + return Result.success(); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/AiGenImageWeb.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/AiGenImageWeb.java new file mode 100644 index 0000000..2c68b67 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/AiGenImageWeb.java @@ -0,0 +1,64 @@ +package com.sonic.cow.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.IpAddressUtils; +import com.sonic.common.utils.RedisLock; +import com.sonic.cow.domain.input.GenImageInput; +import com.sonic.cow.domain.input.GetGenImageInput; +import com.sonic.cow.domain.output.GenImageListOutput; +import com.sonic.cow.domain.output.GenImageOutput; +import com.sonic.cow.service.GenImageTaskService; +import com.sonic.cow.utils.RedisKeyUtils; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Web-AI生成图片 + * + * @author: mzc + * @date: 2025-07-11 11:43 + **/ +@RestController +@Slf4j +public class AiGenImageWeb { + + @Autowired + private GenImageTaskService genImageTaskService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @ApiOperation(value = "创建生成图片任务", tags = {"Web-AI生成图片"}) + @PostMapping(value = "/web/gen/image-ct") + public Result genImageCreate(@RequestBody GenImageInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + input.setIp(IpAddressUtils.getIpAddress(request)); + input.setDeviceId(session.getDeviceCode()); + AtomicReference atomicReference = new AtomicReference(); + //操作人加锁,防止并发操作 + RedisLock redisLock = new RedisLock(redisKeyUtils.genImageLockKey(session.getUserId()), redisWrapper); + redisLock.tryAcquireRun(3 * 1000L, () -> { + GenImageOutput output = genImageTaskService.genImage(session, input); + atomicReference.set(output); + return true; + }); + return Result.success(atomicReference.get()); + } + + @ApiOperation(value = "轮询查询图片生成结果", tags = {"Web-AI生成图片"}) + @PostMapping(value = "/web/gen/image-pl") + public Result> poolingGetGenImage(@RequestBody GetGenImageInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + //轮询任务接口,前端每5s请求一次,超过10s未请求的话直接关闭掉任务 + return Result.success(genImageTaskService.getImage(session.getUserId(), input.getBatchNo())); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/AiMessageWeb.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/AiMessageWeb.java new file mode 100644 index 0000000..e9dd9f3 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/AiMessageWeb.java @@ -0,0 +1,38 @@ +package com.sonic.cow.controller.web; + +import com.sonic.common.auth.IgnoreAuth; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.cow.domain.input.AiMessageDelInput; +import com.sonic.cow.service.AiMessageService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * Web-AI消息 + * + * @author: mzc + * @date: 2025-07-11 11:43 + **/ +@RestController +@Slf4j +public class AiMessageWeb { + + @Autowired + private AiMessageService aiMessageService; + + @IgnoreAuth + @ApiOperation(value = "删除ai消息", tags = {"Web-AI消息"}) + @PostMapping(value = "/web/ai-message/del") + public Result del(@RequestBody AiMessageDelInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + aiMessageService.aiMessageDelete(input, session.getUserId()); + return Result.success(); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/NsfwCheckWeb.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/NsfwCheckWeb.java new file mode 100644 index 0000000..24692d4 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/NsfwCheckWeb.java @@ -0,0 +1,32 @@ +package com.sonic.cow.controller.web; + +import com.sonic.common.rpc.Result; +import com.sonic.cow.client.ContextChatClient; +import com.sonic.cow.lib.input.NsfwCheckInput; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * 敏感词校验 + * + * @author code + */ +@RestController +@Slf4j +public class NsfwCheckWeb { + + @Autowired + private ContextChatClient contextChatClient; + + @ApiOperation(value = "校验敏感词", tags = {"WEB-敏感词校验"}) + @PostMapping("/web/content/check") + public Result contentCheck(@RequestBody NsfwCheckInput input) { + return Result.success(contextChatClient.nsfwCheckV2(input.getContent())); + } + +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/VoiceChatWeb.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/VoiceChatWeb.java new file mode 100644 index 0000000..0c50f7a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/VoiceChatWeb.java @@ -0,0 +1,74 @@ +package com.sonic.cow.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.cow.domain.input.GenerateRtcTokenInput; +import com.sonic.cow.domain.input.VoiceChatOptInput; +import com.sonic.cow.domain.output.GenerateRtcTokenOutput; +import com.sonic.cow.limit.RequestLimit; +import com.sonic.cow.service.VoiceChatService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Web-AI语音通话 + * + * @author: mzc + * @date: 2025-07-30 11:43 + **/ +@RestController +@Slf4j +public class VoiceChatWeb { + + @Autowired + private VoiceChatService voiceChatService; + + + @RequestLimit(count = 100, time = 86400000) + @ApiOperation(value = "获取RTC token", tags = {"Web-AI语音通话"}) + @PostMapping(value = "/web/voice-chat/gen-rtc-tk") + public Result genRTCTk(@RequestBody GenerateRtcTokenInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + return Result.success(voiceChatService.generateRtcToken(session.getUserId(), input)); + } + + @RequestLimit(count = 100, time = 86400000) + @ApiOperation(value = "开始,打断,结束语音通话", tags = {"Web-AI语音通话"}) + @PostMapping(value = "/web/voice-chat/opt") + public Result voiceChatOpt(@RequestBody VoiceChatOptInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + input.setEndpoint(session.getEndpoint()); + return Result.success(voiceChatService.voiceChatOpt(session.getUserId(), input)); + } + + @IgnoreAuth + @ApiOperation(value = "RTC webhook回调通知", tags = {"Web-AI语音通话"}) + @PostMapping(value = "/web/voice-chat/webhook") + public Result webhook(@ApiParam(hidden = true) HttpServletRequest request, @ApiParam(hidden = true) HttpServletResponse response) { + voiceChatService.webhook(request, response); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "会话状态回调通知(监听智能体状态)", tags = {"Web-AI语音通话"}) + @PostMapping(value = "/web/voice-chat/conversation-state-callback") + public Result conversationStateCallback(@ApiParam(hidden = true) HttpServletRequest request, @ApiParam(hidden = true) HttpServletResponse response) { + voiceChatService.conversationStateCallback(request, response); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "实时字幕回调", tags = {"Web-AI语音通话"}) + @PostMapping(value = "/web/voice-chat/rts-callback") + public Result rtsCallback(@ApiParam(hidden = true) HttpServletRequest request, @ApiParam(hidden = true) HttpServletResponse response) { + voiceChatService.rtsCallback(request, response); + return Result.success(); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/VoiceWeb.java b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/VoiceWeb.java new file mode 100644 index 0000000..a7bf126 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/controller/web/VoiceWeb.java @@ -0,0 +1,55 @@ +package com.sonic.cow.controller.web; + +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.cow.domain.input.VoiceAsrInput; +import com.sonic.cow.domain.input.VoiceTtsV2Input; +import com.sonic.cow.domain.output.AsrOutput; +import com.sonic.cow.limit.RequestLimit; +import com.sonic.cow.service.VoiceService; +import com.sonic.cow.service.VoiceV2Service; +import com.sonic.cow.utils.Base64Utils; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.util.UUID; + +/** + * Web-语音处理 + */ +@RestController +@Slf4j +public class VoiceWeb { + + @Autowired + private VoiceService voiceService; + @Autowired + private VoiceV2Service voiceV2Service; + + @RequestLimit(count = 100, time = 86400000) + @ApiOperation(value = "语音转文本V3版本", tags = {"Web-语音处理"}) + @PostMapping(value = "/web/voice/asr-v3") + public Result asrV3(@RequestBody VoiceAsrInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) throws UnirestException { + //保存到临时 File + String suffix = StringUtils.isEmpty(input.getSuffix()) ? ".mp3" : input.getSuffix(); + File tempFile = Base64Utils.decodeToTempFile(input.getData(), UUID.randomUUID() + suffix); + return Result.success(voiceV2Service.asr(session.getUserId(), input.getAiId(), tempFile)); + } + + @RequestLimit(count = 100, time = 86400000) + @ApiOperation(value = "生成语音V2版本", tags = {"Web-语音处理"}) + @PostMapping(value = "/web/voice/tts-v2") + public Result ttsV2(@RequestBody VoiceTtsV2Input input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + return Result.success(voiceService.tts(session.getUserId(), input)); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/dao/ContextSessionCacheDao.java b/sonic-cow/server/src/main/java/com/sonic/cow/dao/ContextSessionCacheDao.java new file mode 100644 index 0000000..a80b032 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/dao/ContextSessionCacheDao.java @@ -0,0 +1,12 @@ +package com.sonic.cow.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.cow.domain.entity.ContextSessionCache; +import org.springframework.stereotype.Repository; + +@Repository +public interface ContextSessionCacheDao extends BaseMapper { + + + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/dao/GenImageTaskDao.java b/sonic-cow/server/src/main/java/com/sonic/cow/dao/GenImageTaskDao.java new file mode 100644 index 0000000..902bdb6 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/dao/GenImageTaskDao.java @@ -0,0 +1,37 @@ +package com.sonic.cow.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.cow.domain.entity.GenImageTask; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author code + */ +public interface GenImageTaskDao extends BaseMapper { + + /** + * 更新任务的已完成数和执行状态 + * @param id + * @param completedCount + * @return + */ + int updateCompletedCount(@Param("id") Long id, @Param("completedCount") Integer completedCount); + + /** + * 更新心跳数据 + * @param batchNo + * @return + */ + int updateHeartBeat(@Param("batchNo") String batchNo); + + /** + * 扫描心跳过期的是数据 + * @param startTime + * @return + */ + List scanHeartBeatExpTime(@Param("startTime") LocalDateTime startTime); + +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/dao/GenImageTaskRecordDao.java b/sonic-cow/server/src/main/java/com/sonic/cow/dao/GenImageTaskRecordDao.java new file mode 100644 index 0000000..6d313d6 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/dao/GenImageTaskRecordDao.java @@ -0,0 +1,13 @@ +package com.sonic.cow.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.cow.domain.entity.GenImageTaskRecord; + +/** + * @author code + */ +public interface GenImageTaskRecordDao extends BaseMapper { + + + +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/dao/PromptConfigDao.java b/sonic-cow/server/src/main/java/com/sonic/cow/dao/PromptConfigDao.java new file mode 100644 index 0000000..79ae2a8 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/dao/PromptConfigDao.java @@ -0,0 +1,12 @@ +package com.sonic.cow.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.cow.domain.entity.PromptConfig; +import org.springframework.stereotype.Repository; + +@Repository +public interface PromptConfigDao extends BaseMapper { + + + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/dao/VoiceChatRecordDao.java b/sonic-cow/server/src/main/java/com/sonic/cow/dao/VoiceChatRecordDao.java new file mode 100644 index 0000000..fd6e4f9 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/dao/VoiceChatRecordDao.java @@ -0,0 +1,12 @@ +package com.sonic.cow.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.cow.domain.entity.VoiceChatRecord; + +/** + * 语音聊天记录 + * @author code + */ +public interface VoiceChatRecordDao extends BaseMapper { + +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/AiChatPromptConfigBo.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/AiChatPromptConfigBo.java new file mode 100644 index 0000000..8fd363f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/AiChatPromptConfigBo.java @@ -0,0 +1,116 @@ +package com.sonic.cow.domain.bo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Data +public class AiChatPromptConfigBo { + + private String userId; + + private String aiId; + + /** + * ai的昵称 + */ + private String aiNickname; + + /** + * ai的性别 + */ + private String aiSex; + + /** + * ai的生日 + */ + private String aiBirthday; + + /** + * ai的个性特征 + */ + private String aiPersonalityTraits; + + /** + * ai的简介 + */ + private String aiProfile; + + /** + * ai的对话风格 + */ + private String aiDialogueStyle; + + /** + * 用户的昵称 + */ + private String userNickname; + + /** + * 用户的性别 + */ + private String userSex; + + /** + * 用户的生日 + */ + private String userBirthday; + + /** + * 用户的简介 + */ + private String userProfile; + + /** + * 用户关系阶段的描述词 + */ + private String relationshipDesc; + + /** + * 用户关系阶段 + */ + private String relationship; + + /** + * 用户关系阶段中被禁止使用的词语 + */ + private String relationshipBanned; + + /** + * 用户与ai关系已经认识的天数 + */ + private int daysKnown; + + /** + * 对话场景 + */ + private String dialogueScenario; + + /** + * 对话开场白 + */ + private String dialoguePrologue; + + private String aiRole; + + private String aiRoleBackstory; + + private String aiRolePrinciple_1; + + private String aiRolePrinciple_2; + + private String aiPrimaryGoal; + + private String aiSecondaryGoal; + + private String aiPositive; + + private String aiNegative; + + private String aiLocation; + private String aiTime; + private String aiSensoryDetails; + private String aiKeyObjects; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/AiGen4Bo.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/AiGen4Bo.java new file mode 100644 index 0000000..1fe5d94 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/AiGen4Bo.java @@ -0,0 +1,24 @@ +package com.sonic.cow.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class AiGen4Bo { + + private String input; + + private String output; + + private String url; + + private String apikey; + + private String content; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/AiUserCacheInfo.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/AiUserCacheInfo.java new file mode 100644 index 0000000..b599018 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/AiUserCacheInfo.java @@ -0,0 +1,46 @@ +package com.sonic.cow.domain.bo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: ai用户缓存信息,用于构建聊天系统提示词,图片提示词 + * @author: mzc + * @date: 2025-07-18 10:36 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserCacheInfo { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("AI所属用户id") + private Long userId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("出生日期") + private Integer age; + + @ApiModelProperty("人物设定") + private String profile; + + @ApiModelProperty("对话风格") + private String dialogueStyle; + + @ApiModelProperty("对话开场白") + private String dialoguePrologue; + + @ApiModelProperty("基图-首次创建AI时选择的形象图") + private String baseImageUrl; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ChatOutputTextBo.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ChatOutputTextBo.java new file mode 100644 index 0000000..2229400 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ChatOutputTextBo.java @@ -0,0 +1,20 @@ +package com.sonic.cow.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ChatOutputTextBo { + + private String message; + + private BigDecimal score; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ExtJsonBo.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ExtJsonBo.java new file mode 100644 index 0000000..d5620b8 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ExtJsonBo.java @@ -0,0 +1,35 @@ +package com.sonic.cow.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ExtJsonBo { + + private String aiRole; + + private String aiRoleBackstory; + + private String aiRolePrinciple_1; + + private String aiRolePrinciple_2; + + private String aiPrimaryGoal; + + private String aiSecondaryGoal; + + private String aiPositive; + + private String aiNegative; + + private String aiLocation; + private String aiTime; + private String aiSensoryDetails; + private String aiKeyObjects; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ImMessageVoiceCallAttachBo.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ImMessageVoiceCallAttachBo.java new file mode 100644 index 0000000..ffc7b49 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ImMessageVoiceCallAttachBo.java @@ -0,0 +1,28 @@ +package com.sonic.cow.domain.bo; + +import com.sonic.cow.client.request.InputRequest; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: IM消息语音通话内容bo + * @author: mzc + * @date: 2025-09-25 13:58 + **/ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ImMessageVoiceCallAttachBo { + + @ApiModelProperty("类型") + private String type; + + @ApiModelProperty("语音通话内容") + private String voiceChatInputRequestList; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ImageGenTextPromptParse.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ImageGenTextPromptParse.java new file mode 100644 index 0000000..19bf734 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/ImageGenTextPromptParse.java @@ -0,0 +1,24 @@ +package com.sonic.cow.domain.bo; + +import com.alibaba.fastjson.JSON; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: 图生文 6组不同prompt + * @author: mzc + * @date: 2025-07-28 17:17 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ImageGenTextPromptParse { + @ApiModelProperty("内容") + private String content; +} + + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/LimitBo.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/LimitBo.java new file mode 100644 index 0000000..cf918b2 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/LimitBo.java @@ -0,0 +1,30 @@ +package com.sonic.cow.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author code + * @Description 限流出参结果对象 + * @Date 2024/3/7 11:00 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LimitBo { + + /** + * 是否被限流 + */ + private Boolean limitBl; + + /** + * 过期时间 + */ + private Long expTime; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/TextGenImagePromptParse.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/TextGenImagePromptParse.java new file mode 100644 index 0000000..592866c --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/TextGenImagePromptParse.java @@ -0,0 +1,43 @@ +package com.sonic.cow.domain.bo; + +import com.alibaba.fastjson.JSON; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: 图生文 6组不同prompt + * @author: mzc + * @date: 2025-07-28 17:17 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TextGenImagePromptParse { + @ApiModelProperty("指令1") + private String prompt1; + + @ApiModelProperty("指令2") + private String prompt2; + + @ApiModelProperty("指令3") + private String prompt3; + + @ApiModelProperty("指令4") + private String prompt4; + + @ApiModelProperty("指令5") + private String prompt5; + + @ApiModelProperty("指令6") + private String prompt6; + + public static void main(String[] args) { + System.out.println(JSON.toJSONString(new TextGenImagePromptParse("value","value","value","value","value","value"))); + } +} + + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/TextPromptParse.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/TextPromptParse.java new file mode 100644 index 0000000..9ed2184 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/bo/TextPromptParse.java @@ -0,0 +1,23 @@ +package com.sonic.cow.domain.bo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: 图生文 6组不同prompt + * @author: mzc + * @date: 2025-07-28 17:17 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TextPromptParse { + @ApiModelProperty("内容") + private String content; +} + + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/ContextSessionCache.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/ContextSessionCache.java new file mode 100644 index 0000000..84bfd8a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/ContextSessionCache.java @@ -0,0 +1,65 @@ +package com.sonic.cow.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 上下文ID的session缓存映射数据实体类 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("context_session_cache") +public class ContextSessionCache { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + @TableField("user_id") + private Long userId; + + /** + * 目标用户ID + */ + @TableField("to_user_id") + private Long toUserId; + + /** + * 上下文ID + */ + @TableField("context_id") + private String contextId; + + /** + * 过期时间 + */ + @TableField("exp_time") + private LocalDateTime expTime; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/GenImageTask.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/GenImageTask.java new file mode 100644 index 0000000..4a9f5df --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/GenImageTask.java @@ -0,0 +1,100 @@ +package com.sonic.cow.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 生成图片任务主表 + * @author code + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "gen_image_task", autoResultMap = true) +public class GenImageTask { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 批次号 + */ + private String batchNo; + + /** + * 用户ID + */ + private Long userId; + + /** + * 状态(PENDING 处理中、RELEASED 已释放) + */ + private Status status; + + /** + * 任务总数 + */ + private Integer taskCount; + + /** + * 已经有多少张图片任务获取到结果了 + */ + private Integer completedCount; + + /** + * 前端轮询次数 + */ + private Integer pollingCount; + + /** + * 最后一次心跳时间 + */ + private LocalDateTime heartBeatTime; + + /** + * 终端类型 + */ + private String endpoint; + + /** + * 设备号 + */ + private String deviceId; + + /** + * IP地址 + */ + private String ipAddress; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + private LocalDateTime editTime; + + + public enum Status { + /** + * 处理中、已释放 + */ + PENDING, + RELEASED; + + } + +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/GenImageTaskRecord.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/GenImageTaskRecord.java new file mode 100644 index 0000000..0f4eec6 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/GenImageTaskRecord.java @@ -0,0 +1,120 @@ +package com.sonic.cow.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 生成图片任务子项表 + * @author code + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "gen_image_task_record", autoResultMap = true) +public class GenImageTaskRecord { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 任务ID gen_image_task表主键id + */ + private Long taskId; + + /** + * 批次号 + */ + private String batchNo; + + /** + * 生图类型 文生图:1 图生图:2 + */ + private Integer type; + + /** + * 用户ID + */ + private Long userId; + + /** + * 生成图片的第三方任务id + */ + private String thirdTaskId; + + /** + * 图生图-生成图片异步任务id,用此查询生图结果 + */ + private String asyncTaskId; + + /** + * 生成图片的SD提示词 + */ + private String prompt; + + /** + * 图片链接 + */ + private String imageUrl; + + /** + * 图片连接的MD5值 + */ + private String imageUrlMd5; + + /** + * 图片生成状态 + */ + private Status status; + + /** + * 图片生成失败原因 + */ + private String failReason; + + /** + * 是否已生成完成(0 否、1 是) + */ + private Boolean completed; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + private LocalDateTime editTime; + + /** + * 图片生成结果状态 + */ + public enum Status { + /** + * 生成中 + * 被鉴黄(终态) + * 生成完成(终态) + * 失败 模型接口报错时 + */ + PENDING, + NSFW, + COMPLETED, + FAILED, + + ; + } + + + +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/PromptConfig.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/PromptConfig.java new file mode 100644 index 0000000..59f0f2c --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/PromptConfig.java @@ -0,0 +1,126 @@ +package com.sonic.cow.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 提示词配置实体类 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("prompt_config") +public class PromptConfig { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 提示词类型 + */ + @TableField("prompt_type") + private String promptType; + + /** + * 提示词名称 + */ + @TableField("prompt_name") + private String promptName; + + /** + * 提示词描述 + */ + @TableField("prompt_desc") + private String promptDesc; + + /** + * 提示词具体内容 + */ + @TableField("prompt_template") + private String promptTemplate; + + /** + * 提示词具体内容 + */ + @TableField("prompt_template_1") + private String promptTemplate1; + + /** + * 提示词温度 + */ + @TableField("temperature") + private Double temperature; + + /** + * 模型 + */ + @TableField("model") + private String model; + + /** + * 召回率 + */ + @TableField("topp") + private Double topp; + + /** + * 频率惩罚 + */ + @TableField("frequency") + private Double frequency; + + /** + * 存在惩罚 + */ + @TableField("presence") + private Double presence; + + /** + * 结构化输出模式 + */ + @TableField("structured_output_schema") + private String structuredOutputSchema; + + /** + * 结构化输出模式名称 + */ + @TableField("structured_output_schema_name") + private String structuredOutputSchemaName; + + /** + * 结构化输出模式描述 + */ + @TableField("structured_output_schema_desc") + private String structuredOutputSchemaDesc; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 创建时间 + */ + @TableField("is_delete") + private Boolean isDelete; + + /** + * 编辑时间 + */ + @TableField(value = "edit_time") + private LocalDateTime editTime; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/VoiceChatRecord.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/VoiceChatRecord.java new file mode 100644 index 0000000..13d1320 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/entity/VoiceChatRecord.java @@ -0,0 +1,74 @@ +package com.sonic.cow.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 语音聊天记录 + * @author code + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "voice_chat_record", autoResultMap = true) +public class VoiceChatRecord { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户id + */ + private Long userId; + + /** + * AI的id + */ + private Long aiId; + + /** + * 房间号 + */ + private String roomId; + + /** + * 任务id + */ + private String taskId; + + /** + * 通话时长 + */ + private Long duration; + + /** + * 通话状态 状态 1:通话中 2:打断 3: 结束 + */ + private Integer status; + + /** + * 端点 + */ + private String endpoint; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiCreateGenSupContentInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiCreateGenSupContentInput.java new file mode 100644 index 0000000..1ccdf39 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiCreateGenSupContentInput.java @@ -0,0 +1,25 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 生成AI辅助对话内容 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AiCreateGenSupContentInput { + + @ApiParam(value = "用户ID") + private Long userId; + + @ApiParam(value = "AI的ID") + private Long aiId; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiGenImageInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiGenImageInput.java new file mode 100644 index 0000000..557fbd5 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiGenImageInput.java @@ -0,0 +1,47 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2024-03-08 11:31 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AiGenImageInput { + + @ApiModelProperty("形象风格提示词") + private String imageStylePrompt; + + @ApiModelProperty("形象描述内容") + private String content; + + @ApiModelProperty("形象参考图") + private String imageReferenceUrl; + + @ApiModelProperty("二创:换脸的图片url(基图,首次创建AI,选择的形象图,创作相册图片时用到)") + private String imageUrl; + + @ApiModelProperty("二创:相册照片或编辑形象时,需要传初始的prompt(初始选择的形象图prompt经过Ai提炼后的prompt)") + private String initPrompt; + + @ApiParam(value = "性别(英文)") + private String sex; + + @ApiParam(value = "生日(yyyy-MM-dd)") + private String birthday; + + @ApiParam(value = "年龄") + private Integer age; + + @ApiModelProperty("简介") + private String introduction; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiGenMockInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiGenMockInput.java new file mode 100644 index 0000000..07b5454 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiGenMockInput.java @@ -0,0 +1,29 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AiGenMockInput { + + @ApiModelProperty("系统提示词") + private String systemPrompt; + + @ApiModelProperty("温度值") + private Double temperatur; + + private Long fromUserId; + + private Long userId; + + private Long aiId; + + private String message; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiMessageDelInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiMessageDelInput.java new file mode 100644 index 0000000..179b507 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/AiMessageDelInput.java @@ -0,0 +1,26 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-09-23 11:12 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AiMessageDelInput { + + @ApiModelProperty("aiId") + @NotNull + private List aiIdList; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/CheckImageIsAIGeneratedInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/CheckImageIsAIGeneratedInput.java new file mode 100644 index 0000000..48effbe --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/CheckImageIsAIGeneratedInput.java @@ -0,0 +1,28 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + + +/** + * @description: + * @author: mzc + * @date: 2025-09-18 11:03 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CheckImageIsAIGeneratedInput { + + @ApiModelProperty("用户id") + private Long userId; + + @ApiModelProperty("图片url列表") + private List imgUrlList; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/DemoInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/DemoInput.java new file mode 100644 index 0000000..3f4672d --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/DemoInput.java @@ -0,0 +1,19 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 入参实体对象 + * @author code + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DemoInput { + + @ApiModelProperty(value = "用户ID") + private Long id; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiDialogStyleAndPrologueInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiDialogStyleAndPrologueInput.java new file mode 100644 index 0000000..32c5392 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiDialogStyleAndPrologueInput.java @@ -0,0 +1,31 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:10 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenAiDialogStyleAndPrologueInput { + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private String sex; + + @ApiModelProperty("年龄") + private Integer age; + + @ApiModelProperty("人物设定") + private String profile; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiImageContentInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiImageContentInput.java new file mode 100644 index 0000000..8953f08 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiImageContentInput.java @@ -0,0 +1,33 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: 生成AI人物的基础信息入参 + * @author: mzc + * @date: 2024-01-04 14:12 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class GenAiImageContentInput { + + @ApiParam(value = "昵称") + private String nickname; + + @ApiParam(value = "性别(英文)") + private String sex; + + @ApiParam(value = "年龄") + private Integer age; + + @ApiModelProperty("ai的人物基础信息(背景、性格、身份)") + private String figure; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiImageDescInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiImageDescInput.java new file mode 100644 index 0000000..4891db0 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiImageDescInput.java @@ -0,0 +1,31 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:10 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenAiImageDescInput { + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("年龄") + private Integer age; + + @ApiModelProperty("人物设定") + private String profile; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiIntroductionInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiIntroductionInput.java new file mode 100644 index 0000000..f988b7d --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiIntroductionInput.java @@ -0,0 +1,31 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:10 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenAiIntroductionInput { + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("年龄") + private Integer age; + + @ApiModelProperty("人物设定") + private String profile; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiProfileInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiProfileInput.java new file mode 100644 index 0000000..01b014c --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiProfileInput.java @@ -0,0 +1,28 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:00 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenAiProfileInput { + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("年龄") + private Integer age; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiUserContentInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiUserContentInput.java new file mode 100644 index 0000000..fceda01 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiUserContentInput.java @@ -0,0 +1,59 @@ +package com.sonic.cow.domain.input; + +import com.sonic.cow.enums.PromptTypeEnum; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * 生成AI人物的基础信息入参 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class GenAiUserContentInput { + + @ApiParam(value = "昵称") + private String nickname; + + @ApiParam(value = "性别(英文)") + private String sex; + + @ApiParam(value = "生日(yyyy-MM-dd)") + private String birthday; + + @ApiParam(value = "年龄") + private Integer age; + + @ApiParam(value = "性格(字典代码)") + private String characterCode; + + @ApiParam(value = "标签(字典代码)") + private String tagCode; + + @ApiModelProperty("ai的人物基础信息(背景、性格、身份)") + private String figure; + + @ApiModelProperty("ai对话风格(角色的聊天方式、对话语气)") + private String dialogue; + + @ApiModelProperty("ai开场白信息") + private String prologue; + + @ApiModelProperty("ai形象描述信息(肤色、服饰、发型、五官、动作、背景等)") + private String appearance; + + @ApiModelProperty("ai的个人简介") + private String introduction; + + @NotNull + @ApiModelProperty("提示词类型") + private PromptTypeEnum ptType; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiUserContentV1Input.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiUserContentV1Input.java new file mode 100644 index 0000000..81adf49 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAiUserContentV1Input.java @@ -0,0 +1,56 @@ +package com.sonic.cow.domain.input; + +import com.sonic.cow.enums.PromptTypeEnum; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * 生成AI人物的基础信息入参 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class GenAiUserContentV1Input { + + @ApiParam(value = "昵称") + private String nickname; + + @ApiParam(value = "性别(英文)") + private String sex; + + @ApiParam(value = "生日(yyyy-MM-dd)") + private String birthday; + + @ApiParam(value = "年龄") + private Integer age; + + @ApiParam(value = "性格(字典代码)") + private String characterCode; + + @ApiParam(value = "标签(字典代码)") + private String tagCode; + + @ApiModelProperty("用户输入的内容") + private String content; + + @ApiModelProperty("ai的人物基础信息(背景、性格、身份)【生成个人简介时使用】") + private String figure; + + @ApiModelProperty("ai对话风格(角色的聊天方式、对话语气)【生成个人简介时使用】") + private String dialogue; + + @NotNull + @ApiModelProperty("提示词类型") + private PromptTypeEnum ptType; + + @ApiModelProperty("简介") + private String introduction; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAutoChatContentInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAutoChatContentInput.java new file mode 100644 index 0000000..6ea3160 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenAutoChatContentInput.java @@ -0,0 +1,24 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * 生成AI自动聊天内容 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class GenAutoChatContentInput { + + @NotNull + @ApiParam(value = "AI的ID") + private Long aiId; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenImageCallbackInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenImageCallbackInput.java new file mode 100644 index 0000000..9f349ee --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenImageCallbackInput.java @@ -0,0 +1,50 @@ +package com.sonic.cow.domain.input; + +import lombok.Data; + +/** + * @Author code + * @Description 生成图片结果回调 + * @Date 2024/3/8 10:46 + * @Version 1.0 + */ +@Data +public class GenImageCallbackInput { + + /** + * 图片生成任务ID + */ + private String thirdTaskId; + + /** + * 图生图-生成图片异步任务id,用此查询生图结果 + */ + private String asyncTaskId; + + + /** + * 图片生成提示词 + */ + private String prompt; + + /** + * 图片链接地址 + */ + private String imageUrl; + + /** + * 图片状态(NSFW 图片被鉴黄、COMPLETED 生成完成) + */ + private String status; + + /** + * 图片生成失败原因 + */ + private String failReason; + + /** + * 图片生成任务类型 1:文生图 2:图生图 + */ + private Integer type; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenImageInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenImageInput.java new file mode 100644 index 0000000..0fd363d --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenImageInput.java @@ -0,0 +1,53 @@ +package com.sonic.cow.domain.input; + +import com.sonic.cow.enums.CreateImageType; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.Data; +import lombok.Getter; + +/** + * @Author code + * @Description 图片生成入参 + * @Date 2024/3/6 17:22 + * @Version 1.0 + */ +@Data +public class GenImageInput { + + @ApiModelProperty("类型") + private CreateImageType type; + + @ApiParam(value = "ai的id") + private Long aiId; + + @ApiModelProperty("形象风格提示词") + private String imageStylePrompt; + + @ApiModelProperty("形象描述内容") + private String content; + + @ApiModelProperty("形象参考图 data:image/图片类型(png,jpeg);base64,base64值") + private String imageReferenceUrl; + + @ApiParam(value = "换脸") + private Boolean hl = false; + + @ApiParam(value = "性别(英文)") + private String sex; + + @ApiParam(value = "生日(yyyy-MM-dd)") + private String birthday; + + private String deviceId; + + private String ip; + + private String endpoint; + + @ApiParam(value = "年龄") + private Integer age; + + @ApiModelProperty("简介") + private String introduction; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenSupContentInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenSupContentInput.java new file mode 100644 index 0000000..57c7fcd --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenSupContentInput.java @@ -0,0 +1,29 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 生成AI辅助对话内容 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class GenSupContentInput { + + @ApiParam(value = "AI的ID") + private Long aiId; + + @ApiParam(value = "批次编号") + private String batchNo; + + @ApiParam(value = "需要排除的内容列表") + private List excContentList; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenerateRtcTokenInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenerateRtcTokenInput.java new file mode 100644 index 0000000..2dcdd6d --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GenerateRtcTokenInput.java @@ -0,0 +1,25 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-30 11:20 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class GenerateRtcTokenInput { + + /** + * 房间id + */ + @ApiModelProperty(value = "房间id") + private String roomId; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GetGenImageInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GetGenImageInput.java new file mode 100644 index 0000000..906d997 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GetGenImageInput.java @@ -0,0 +1,17 @@ +package com.sonic.cow.domain.input; + +import lombok.Data; + +/** + * 获取生成图片的结果 + * + * @author mzc + */ +@Data +public class GetGenImageInput { + + /** + * 批次号 + */ + String batchNo; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GetGenImageListInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GetGenImageListInput.java new file mode 100644 index 0000000..d1bb74f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/GetGenImageListInput.java @@ -0,0 +1,23 @@ +package com.sonic.cow.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 15:30 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GetGenImageListInput { + + /** + * 批次号 + */ + private String batchNo; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/MockInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/MockInput.java new file mode 100644 index 0000000..62dc968 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/MockInput.java @@ -0,0 +1,25 @@ + +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 生成AI辅助对话内容 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class MockInput { + + @ApiParam(value = "用户的ID") + private Long userId; + + @ApiParam(value = "AI的ID") + private Long aiId; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/TtsDeAmountInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/TtsDeAmountInput.java new file mode 100644 index 0000000..99ad7ef --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/TtsDeAmountInput.java @@ -0,0 +1,24 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class TtsDeAmountInput { + + @ApiModelProperty("授权密钥") + private String sk; + + @ApiModelProperty("当前用户ID") + private Long userId; + + @ApiModelProperty("AI id") + private Long aiId; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceAsrInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceAsrInput.java new file mode 100644 index 0000000..a8a5038 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceAsrInput.java @@ -0,0 +1,26 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@NoArgsConstructor +@Data +public class VoiceAsrInput { + + @NotNull + @ApiModelProperty("AI id") + private Long aiId; + + @ApiModelProperty("base64编码音频内容(v1版本使用)") + private String data; + + @ApiModelProperty("音频url(v2版本使用)") + private String url; + + @ApiModelProperty("音频文件后缀") + private String suffix; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceChatOptInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceChatOptInput.java new file mode 100644 index 0000000..e1bffd4 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceChatOptInput.java @@ -0,0 +1,44 @@ +package com.sonic.cow.domain.input; + +import com.sonic.cow.enums.VoiceChatOptTypeEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-08-14 14:09 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VoiceChatOptInput { + + @ApiModelProperty("ai的id") + @NotNull + private Long aiId; + + @ApiModelProperty("操作类型 开启通话:START,打断:INTERRUPT,结束通话:STOP,取消通话:CANCEL") + @NotNull + private VoiceChatOptTypeEnum optType; + + @ApiModelProperty("房间id") + @NotNull + private String roomId; + + @ApiModelProperty("任务id") + @NotNull + private String taskId; + + @ApiModelProperty("结束通话时传 通话时长") + private Long duration; + + @ApiModelProperty("终端(内部使用)") + private String endpoint; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceTtsInput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceTtsInput.java new file mode 100644 index 0000000..02a6f6f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceTtsInput.java @@ -0,0 +1,26 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class VoiceTtsInput { + + @ApiModelProperty("文本内容") + private String text; + + @ApiModelProperty("语音类型【传入审核并开通的语音ID】以S_开头的ID") + private String voiceType; + + @ApiModelProperty("语速,范围 [0.2, 3],默认 1.0,通常保留一位小数") + private Float speedRatio = 1.0f; + + @ApiModelProperty("音量调节,范围 [0.5, 2],默认 1.0,通常保留一位小数。备注:0.5 表示原音量 0.5 倍,2 表示原音量 2 倍") + private Float loudnessRatio = 1.0f; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceTtsV2Input.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceTtsV2Input.java new file mode 100644 index 0000000..a5eb278 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/input/VoiceTtsV2Input.java @@ -0,0 +1,32 @@ +package com.sonic.cow.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class VoiceTtsV2Input { + + @ApiModelProperty("用户id 内部使用") + private Long userId; + + @ApiModelProperty("AI id") + private Long aiId; + + @ApiModelProperty("文本内容") + private String text; + + @ApiModelProperty("语音类型【传入审核并开通的语音ID】以S_开头的ID") + private String voiceType; + + @ApiModelProperty("语速,范围 [-50,100],100代表2.0倍速,-50代表0.5倍速") + private Integer speechRate = 0; + + @ApiModelProperty("音量(Volume)。值范围为 [-12, 12]。默认:0") + private Integer pitchRate = 0; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/AiGenImageOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/AiGenImageOutput.java new file mode 100644 index 0000000..7584f3f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/AiGenImageOutput.java @@ -0,0 +1,25 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2024-03-20 16:35 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AiGenImageOutput { + + @ApiModelProperty("任务id") + private String taskId; + + @ApiModelProperty("提示词") + private String prompt; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/AsrOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/AsrOutput.java new file mode 100644 index 0000000..88603fb --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/AsrOutput.java @@ -0,0 +1,21 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AsrOutput { + + @ApiModelProperty("上下文内容") + String content; + + @ApiModelProperty("时长(单位:毫秒)") + Integer duration; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/DemoOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/DemoOutput.java new file mode 100644 index 0000000..c76d0b5 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/DemoOutput.java @@ -0,0 +1,19 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 出参实体对象 + * @author code + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DemoOutput { + + @ApiModelProperty(value = "用户ID") + private Long id; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiDialogStyleAndPrologueOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiDialogStyleAndPrologueOutput.java new file mode 100644 index 0000000..6370424 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiDialogStyleAndPrologueOutput.java @@ -0,0 +1,26 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:11 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenAiDialogStyleAndPrologueOutput { + + @ApiModelProperty("对话风格") + private String dialogueStyle; + + @ApiModelProperty("对话开场白") + private String dialoguePrologue; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiImageContentOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiImageContentOutput.java new file mode 100644 index 0000000..3be5d1f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiImageContentOutput.java @@ -0,0 +1,19 @@ +package com.sonic.cow.domain.output; + +import lombok.Data; + +/** + * @Author code + * @Description 生成AI人物的基础信息 + * @Date 2024/1/3 20:43 + * @Version 1.0 + */ +@Data +public class GenAiImageContentOutput { + + /** + * ai图片文本信息 + */ + private String content; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiImageDescOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiImageDescOutput.java new file mode 100644 index 0000000..9065cfd --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiImageDescOutput.java @@ -0,0 +1,23 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:11 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenAiImageDescOutput { + + @ApiModelProperty("形象描述") + private String imageDesc; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiIntroductionOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiIntroductionOutput.java new file mode 100644 index 0000000..89fe5fd --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiIntroductionOutput.java @@ -0,0 +1,23 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:11 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenAiIntroductionOutput { + + @ApiModelProperty("简介") + private String introduction; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiProfileOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiProfileOutput.java new file mode 100644 index 0000000..f816259 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiProfileOutput.java @@ -0,0 +1,22 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:05 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenAiProfileOutput { + + @ApiModelProperty("AI用户设定") + private String profile; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiUserContentOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiUserContentOutput.java new file mode 100644 index 0000000..a880478 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiUserContentOutput.java @@ -0,0 +1,30 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 生成AI人物的基础信息 + */ +@Data +public class GenAiUserContentOutput { + + @ApiModelProperty("ai的人物基础信息(背景、性格、身份)") + private String figure; + + @ApiModelProperty("ai对话风格(角色的聊天方式、对话语气)") + private String dialogue; + + @ApiModelProperty("ai开场白信息") + private String prologue; + + @ApiModelProperty("ai形象描述信息(肤色、服饰、发型、五官、动作、背景等)") + private String appearance; + + @ApiModelProperty("ai的个人简介") + private String introduction; + + @ApiModelProperty("ai完整的基础信息的系统提示词") + private String profile; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiUserContentV1Output.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiUserContentV1Output.java new file mode 100644 index 0000000..06e99ce --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenAiUserContentV1Output.java @@ -0,0 +1,23 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 生成AI人物的基础信息 + */ +@Data +public class GenAiUserContentV1Output { + + @ApiModelProperty("一键生成的内容") + private String content; + + private String url; + + private String apikey; + + private String input; + + private String output; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenImageListOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenImageListOutput.java new file mode 100644 index 0000000..ce12f61 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenImageListOutput.java @@ -0,0 +1,30 @@ +package com.sonic.cow.domain.output; + +import com.sonic.cow.domain.entity.GenImageTaskRecord; +import lombok.Data; + +/** + * @Author code + * @Description 生成图片出参 + * @Date 2023/8/24 17:51 + * @Version 1.0 + */ +@Data +public class GenImageListOutput { + + /** + * 图片的URL + */ + private String imageUrl; + + /** + * 图片生成状态(PENDING 生成中、NSFW 黄图、COMPLETED 生成完成) + */ + private GenImageTaskRecord.Status status; + + /** + * 提示词 + */ + private String prompt; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenImageOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenImageOutput.java new file mode 100644 index 0000000..ff9e80f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenImageOutput.java @@ -0,0 +1,19 @@ +package com.sonic.cow.domain.output; + +import lombok.Data; + +/** + * @Author code + * @Description 生成图片出参 + * @Date 2023/8/24 17:51 + * @Version 1.0 + */ +@Data +public class GenImageOutput { + + /** + * 批次号 + */ + private String batchNo; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenSupContentOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenSupContentOutput.java new file mode 100644 index 0000000..002b2af --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenSupContentOutput.java @@ -0,0 +1,23 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenSupContentOutput { + + @ApiModelProperty("批次号") + private String batchNo; + + @ApiModelProperty("内容列表") + private List contentList; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenerateRtcTokenOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenerateRtcTokenOutput.java new file mode 100644 index 0000000..37dfee9 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GenerateRtcTokenOutput.java @@ -0,0 +1,22 @@ +package com.sonic.cow.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-30 11:23 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class GenerateRtcTokenOutput { + + @ApiModelProperty("RTC生成的token") + private String token; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GetGenImageListOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GetGenImageListOutput.java new file mode 100644 index 0000000..32878bb --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/GetGenImageListOutput.java @@ -0,0 +1,27 @@ +package com.sonic.cow.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 15:24 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GetGenImageListOutput { + /** + * 图片的URL + */ + private String imageUrl; + + /** + * 图片生成状态(PENDING 生成中、NSFW 黄图、COMPLETED 生成完成) + */ + private String status; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/VoiceChatOptOutput.java b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/VoiceChatOptOutput.java new file mode 100644 index 0000000..55f3a51 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/domain/output/VoiceChatOptOutput.java @@ -0,0 +1,16 @@ +package com.sonic.cow.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-08-14 14:10 + **/ +@Data +public class VoiceChatOptOutput { + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/enums/BizResultCode.java b/sonic-cow/server/src/main/java/com/sonic/cow/enums/BizResultCode.java new file mode 100644 index 0000000..f061702 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/enums/BizResultCode.java @@ -0,0 +1,91 @@ +package com.sonic.cow.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum BizResultCode implements ApiResultCode { + + + /** + * 生成图片超时提示 + */ + GEN_IMAGE_TIMEOUT("8006", "生成图片超时"), + + MISS_PARAM_ERROR("1001010", "Missing parameter"), + + ; + + + private final String errorCode; + private final String errorMsg; + + BizResultCode(String errorCode, String errorMsg) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1002"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + + /** + * 校验方法 + * + * @param expect + * @param code + */ + public void check(boolean expect, String code) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(code, this.name().equals(message) ? this.getErrorMsg() : message); + + } + } + + /** + * 校验方法 + * + * @param expect + * @param code + * @param message + */ + public void check(boolean expect, String code, String message) { + if (expect) { + throw new BizException(code, message); + } + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/enums/BusinessException.java b/sonic-cow/server/src/main/java/com/sonic/cow/enums/BusinessException.java new file mode 100644 index 0000000..4ac92cb --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/enums/BusinessException.java @@ -0,0 +1,39 @@ +package com.sonic.cow.enums; + + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.GlobalResultCode; + +public class BusinessException extends BizException { + + private static final long serialVersionUID = -5317007026578376164L; + + /** + * 错误码 + */ + private String errorCode; + /** + * 错误描述 + */ + private String errorMsg; + + /** + * @param errorCode + * @param errorMsg + */ + public BusinessException(String errorCode, String errorMsg) { + super(GlobalResultCode.INVALID_PARAMS.getErrorCode(), String.format("BusinessException{errorCode:%s, errorMsg:%s}", errorCode, errorMsg)); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + @Override + public String getErrorCode() { + return errorCode; + } + + @Override + public String getErrorMsg() { + return errorMsg; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/enums/CreateImageType.java b/sonic-cow/server/src/main/java/com/sonic/cow/enums/CreateImageType.java new file mode 100644 index 0000000..1c8b534 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/enums/CreateImageType.java @@ -0,0 +1,15 @@ +package com.sonic.cow.enums; + +import lombok.Getter; + +@Getter +public enum CreateImageType { + //创建ai形象 + CREATE_AI_IMAGE, + //编辑ai形象 + EDIT_AI_IMAGE, + //相册 + ALBUM, + //背景 + BACKGROUND +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/enums/DeductionTypeEnum.java b/sonic-cow/server/src/main/java/com/sonic/cow/enums/DeductionTypeEnum.java new file mode 100644 index 0000000..535ad1f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/enums/DeductionTypeEnum.java @@ -0,0 +1,48 @@ +package com.sonic.cow.enums; + +import com.sonic.lion.lib.enums.BizType; +import lombok.Getter; + +@Getter +public enum DeductionTypeEnum { + //文本 + TEXT(1, 100L, BizType.TEXT_MODEL), + //发送或听取语音 + VOICE(2, 1000L, BizType.SEND_VOICE), + //语音通话 + VOICE_CALL(3, 2000L, BizType.VOICE_CALL), + + ; + private final Integer index; + + /** + * 每一次预扣除金额 单位分 + */ + private final Long amount; + + /** + * 对应的付款类型 + */ + private final BizType bizType; + + DeductionTypeEnum(Integer index, Long amount, BizType bizType) { + this.index = index; + this.amount = amount; + this.bizType = bizType; + } + + /** + * 通过索引获取枚举 + * + * @param index + * @return + */ + public static DeductionTypeEnum getDeductionTypeEnum(Integer index) { + for (DeductionTypeEnum value : DeductionTypeEnum.values()) { + if (value.index.equals(index)) { + return value; + } + } + return null; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/enums/PromptTypeEnum.java b/sonic-cow/server/src/main/java/com/sonic/cow/enums/PromptTypeEnum.java new file mode 100644 index 0000000..8fff4aa --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/enums/PromptTypeEnum.java @@ -0,0 +1,72 @@ +package com.sonic.cow.enums; + +public enum PromptTypeEnum { + //AI一键生成人物基础信息 AI自行创作 + GEN_PROFILE_BY_NON, + + //AI一键生成人物基础信息 AI根据用户输入进行创作 + GEN_PROFILE_BY_CONTENT, + + //AI一键生成对话风格 AI自行创作 + GEN_DIALOG_STYLE_BY_NON, + + //AI一键生成对话风格 AI根据用户输入进行创作 + GEN_DIALOG_STYLE_BY_CONTENT, + + //AI一键生成开场白 AI自行创作 + GEN_PROLOGUE_BY_NON, + + //AI一键生成开场白 AI根据用户输入进行创作 + GEN_PROLOGUE_BY_CONTENT, + + //AI一键生成人物简介 AI总结 + GEN_INTRODUCTION, + + //形象描述 AI自行创作 + GEN_AI_IMAGE_DESC_BY_NON, + + //形象描述 AI根据用户输入进行创作 + GEN_AI_IMAGE_DESC_BY_CONTENT, + + //文生图-生成6组不同的prompt + TEXT_TO_IMAGE_PROMPT, + + //图生文-参考图生成prompt + IMAGE_REFERENCE, + + //文生图-生成6组不同的prompt + TXT_TO_IMAGE_PROMPT, + + //聊天时用的系统提示词 + CHAT_SYSTEM_PROMPT_TEMPLATE, + + //超过24小时用户未发送聊天消息时,AI主动给用户发送消息的系统提示词 + AUTO_SEND_CHAT_SYSTEM_PROMPT_TEMPLATE, + + //用户停留3分钟未发送聊天消息时,AI主动给用户发送消息的系统提示词 + AUTO_SEND_CHAT_3_MINUTES_SYSTEM_PROMPT_TEMPLATE, + + //生成9条辅助聊天内容的系统提示词 + GEN_SUP_CONTENT_SYSTEM_PROMPT_TEMPLATE, + + //语音通话时用的系统提示词 + VOICE_CHAT_SYSTEM_PROMPT_TEMPLATE, + + //聊天对话打情绪分值 + CHAT_SCORE_SYSTEM_PROMPT_TEMPLATE, + + //对话场景 + DIALOGUE_SCENARIO, + + //编辑AI或相册图片生成时,合并新老形象描述 + MERGE_NEW_OLD_IMAGE_DESC, + //获取图片的定义 + GET_AI_IMAGE_FUNCTION_CALL, + //开启语音通话时根据上下文生成的对话开场白 + START_VOICE_CHAT_DIALOGUE_PROLOGUE, + + //提取JSON内容 + EXTRACT_JSON_CONTENT, + ; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/enums/SexEnums.java b/sonic-cow/server/src/main/java/com/sonic/cow/enums/SexEnums.java new file mode 100644 index 0000000..7e3cafb --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/enums/SexEnums.java @@ -0,0 +1,47 @@ +package com.sonic.cow.enums; + +import org.apache.commons.lang3.StringUtils; + +/** + * 性别 + */ +public enum SexEnums { + + MALE(0, "Male"), + FEMALE(1, "Female"), + NON(2, "Nonconforming"); + + private Integer index; + + private String desc; + + SexEnums(Integer index, String desc) { + this.index = index; + this.desc = desc; + } + + public Integer getIndex() { + return index; + } + + public String getDesc() { + return desc; + } + + /** + * 性别转换 + * @param index + * @return + */ + public static String getSex(String index) { + if(StringUtils.isEmpty(index)) { + return "Nonconforming"; + } + for (SexEnums sexEnums : SexEnums.values()) { + if(Integer.valueOf(index).equals(sexEnums.getIndex())) { + return sexEnums.getDesc(); + } + } + return "Nonconforming"; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/enums/ToastResultCode.java b/sonic-cow/server/src/main/java/com/sonic/cow/enums/ToastResultCode.java new file mode 100644 index 0000000..c163a73 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/enums/ToastResultCode.java @@ -0,0 +1,99 @@ +package com.sonic.cow.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum ToastResultCode implements ApiResultCode { + SYS_SYSTEM_EXCEPTION("","System exception"), + SYS_PERMISSION_DENIED("0011","Insufficient permissions"), + + + GEN_CONTENT_LIMIT_ERROR("","The maximum number of generations has been exceeded within 24 hours. Please try again later"), + + GEN_IMAGE_LIMIT_ERROR("","You can change your avatar at most 10 times within 24 hours. Please try again in %s hours and %s minutes"), + + SOUND_ASR_ERROR("","Audio recognition failed"), + + AI_NOT_EXIST("","AI does not exist"), + + IMAGE_NOT_ILLEGAL("","Image is illegal"), + + USER_BALANCE_INSUFFICIENT("INSUFFICIENT_BALANCE","Your balance is insufficient!"), + + NSWF_ERROR("", "Please remove the following sensitive content: %s"), + + REQUEST_LIMIT_ERROR("", "The requests are too frequent. Please try again later"), + + + ; + + + private final String errorCode; + private final String errorMsg; + + ToastResultCode(String errorMsg) { + this.errorCode = ""; + this.errorMsg = errorMsg; + } + + ToastResultCode(String errorCode, String errorMsg) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect, String msg) { + if (expect) { + if(StringUtils.isNotBlank(msg)) { + throw new BizException(this.getErrorCode(), msg); + + } else { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/enums/VoiceChatOptTypeEnum.java b/sonic-cow/server/src/main/java/com/sonic/cow/enums/VoiceChatOptTypeEnum.java new file mode 100644 index 0000000..ed2792f --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/enums/VoiceChatOptTypeEnum.java @@ -0,0 +1,18 @@ +package com.sonic.cow.enums; + +import lombok.Getter; + +/** + * 语音通州操作类型枚举 + */ +@Getter +public enum VoiceChatOptTypeEnum { + //开启通话 + START, + //打断 + INTERRUPT, + //结束通话 + STOP, + //取消通话 + CANCEL +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/enums/VoiceChatStatusEnum.java b/sonic-cow/server/src/main/java/com/sonic/cow/enums/VoiceChatStatusEnum.java new file mode 100644 index 0000000..42035d9 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/enums/VoiceChatStatusEnum.java @@ -0,0 +1,19 @@ +package com.sonic.cow.enums; + +import lombok.Getter; + +@Getter +public enum VoiceChatStatusEnum { + + CALLING(1, "通话中"), + INTERRUPT(2, "打断"), + STOP(3, "已结束"); + + private Integer code; + private String desc; + + VoiceChatStatusEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/EventType.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/EventType.java new file mode 100644 index 0000000..612d6d3 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/EventType.java @@ -0,0 +1,59 @@ +package com.sonic.cow.event.inner; + +import com.sonic.cow.config.EventConfig; +import com.sonic.common.event.Event; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义当前系统的消息 + * + * @author chenjun + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** + * 事件定义 + */ + DEMO_CREATED(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "demo_created", "demo 创建"), + /** + * ai生成图片-文生图 + */ + AI_TEXT_GEN_IMAGE(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "ai_text_gen_image", "ai生成图片-文生图"), + + /** + * ai生成图片-图生图 + */ + AI_IMAGE_GEN_IMAGE(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "ai_image_gen_image", "ai生成图片-图生图"), + + /** + * 情绪感知打分 + */ + EMOTION_SCORE(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "emotion_score", "情绪感知打分"), + + /** + * 语音通话预扣费处理 + */ + VOICE_CALL_DEDUCTION(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "voice_call_deduction","语音通话预扣费处理"), + /** + * 语音通话回调事件处理 + */ + VOICE_CALL_WEBHOOK(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "voice_call_webhook","语音通话回调事件处理"), + + + + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; + } diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/AiImageGenImageHandler.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/AiImageGenImageHandler.java new file mode 100644 index 0000000..cdb5e92 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/AiImageGenImageHandler.java @@ -0,0 +1,112 @@ +package com.sonic.cow.event.inner.handler; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.common.utils.RedisLock; +import com.sonic.cow.client.ImageGenImageClient; +import com.sonic.cow.client.Seedream4GenImageClient; +import com.sonic.cow.client.TextGenImageClient; +import com.sonic.cow.domain.input.GenImageCallbackInput; +import com.sonic.cow.event.inner.EventType; +import com.sonic.cow.event.inner.payload.AiGenImagePayload; +import com.sonic.cow.service.GenImageTaskService; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.shark.lib.client.S3Client; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Base64; + +/** + * @author chenjun + */ +@Slf4j +@Component +public class AiImageGenImageHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private TextGenImageClient textGenImageClient; + @Autowired + private ImageGenImageClient imageGenImageClient; + @Autowired + private S3Client s3Client; + @Autowired + private GenImageTaskService genImageTaskService; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private Seedream4GenImageClient seedream4GenImageClient; + + @Override + public void onEvent(Event event) { + AiGenImagePayload payload = event.normalizedData(AiGenImagePayload.class); + log.info("AiImageGenImageHandler payload:{}", payload); +// String imageUrl = payload.getImageUrl(); +// if (StringUtils.isNotEmpty(imageUrl)) { +// //图生图时才加锁,针对批次号加锁 +// String key = redisKeyUtils.aiGenImageLockKey(payload.getBatchNo()); +// RedisLock redisLock = new RedisLock(key, redisWrapper); +// redisLock.tryAcquireRun(25 * 1000, 25 * 1000, () -> { +// aiGenImage(payload); +// return true; +// }); +// } + aiGenImage(payload); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.AI_IMAGE_GEN_IMAGE.getEventCode(), this); + } + + private void aiGenImage(AiGenImagePayload payload) { + String prompt = payload.getPrompt(); + String taskId = payload.getTaskId(); + //基图 + String imageUrl = payload.getImageUrl(); + if (StringUtils.isEmpty(taskId) || StringUtils.isEmpty(prompt)) { + return; + } + //生成的图片 + String base64Json = null; + String asyncTaskId = "0"; + //完成状态 + String status = "COMPLETED"; + String failReason = null; + try { + //调用模型-同步图生图 +// base64Json = imageGenImageClient.imageGenImage(imageUrl, prompt); + base64Json = seedream4GenImageClient.genImage(imageUrl, prompt); + } catch (Exception e) { + log.error("AiImageGenImageHandler error:", e); + status = "FAILED"; + failReason = e.getMessage(); + } + //图片上传,鉴黄,更新taskId状态发送MQ到业务系统处理 + String genImageUrl = null; + if (StringUtils.isNotEmpty(base64Json)) { + //上传到S3 + byte[] imageBytes = Base64.getDecoder().decode(base64Json); + genImageUrl = s3Client.uploadAwsS3(imageBytes, "ROLE","jpg"); + log.info("AiImageGenImageHandler genImageUrl:{}", genImageUrl); + } + //更新生成图片任务状态结果 + GenImageCallbackInput genImageCallbackInput = new GenImageCallbackInput(); + genImageCallbackInput.setThirdTaskId(taskId); + genImageCallbackInput.setPrompt(prompt); + genImageCallbackInput.setImageUrl(genImageUrl); + genImageCallbackInput.setStatus(status); + genImageCallbackInput.setFailReason(failReason); + genImageCallbackInput.setAsyncTaskId(asyncTaskId); + genImageCallbackInput.setType(2); + genImageTaskService.genImageCallback(genImageCallbackInput); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/AiTextGenImageHandler.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/AiTextGenImageHandler.java new file mode 100644 index 0000000..b7f3b6a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/AiTextGenImageHandler.java @@ -0,0 +1,92 @@ +package com.sonic.cow.event.inner.handler; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.cow.client.Seedream4GenImageClient; +import com.sonic.cow.client.TextGenImageClient; +import com.sonic.cow.domain.input.GenImageCallbackInput; +import com.sonic.cow.event.inner.EventType; +import com.sonic.cow.event.inner.payload.AiGenImagePayload; +import com.sonic.cow.service.GenImageTaskService; +import com.sonic.shark.lib.client.S3Client; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Base64; + +/** + * @author chenjun + */ +@Slf4j +@Component +public class AiTextGenImageHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private TextGenImageClient textGenImageClient; + @Autowired + private S3Client s3Client; + @Autowired + private GenImageTaskService genImageTaskService; + @Autowired + private Seedream4GenImageClient seedream4GenImageClient; + + @Override + public void onEvent(Event event) { + AiGenImagePayload payload = event.normalizedData(AiGenImagePayload.class); + log.info("AiTextGenImageHandler payload:{}", payload); + //文生图 + aiGenImage(payload); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.AI_TEXT_GEN_IMAGE.getEventCode(), this); + } + + private void aiGenImage(AiGenImagePayload payload) { + String prompt = payload.getPrompt(); + String taskId = payload.getTaskId(); + if (StringUtils.isEmpty(taskId) || StringUtils.isEmpty(prompt)) { + return; + } + //生成的图片 + String base64Json = null; + String asyncTaskId = "0"; + //完成状态 + String status = "COMPLETED"; + String failReason = null; + try { + //调用模型-文生成图片 +// base64Json = textGenImageClient.textGenImage(prompt); + base64Json = seedream4GenImageClient.genImage("", prompt); + } catch (Exception e) { + log.error("AiTextGenImageHandler error:", e); + status = "FAILED"; + failReason = e.getMessage(); + } + //图片上传,更新taskId状态发送MQ到业务系统处理 + String genImageUrl = null; + if (StringUtils.isNotEmpty(base64Json)) { + //上传到S3 + byte[] imageBytes = Base64.getDecoder().decode(base64Json); + genImageUrl = s3Client.uploadAwsS3(imageBytes, "ROLE","jpg"); + log.info("AiTextGenImageHandler genImageUrl:{}", genImageUrl); + } + //更新生成图片任务状态结果 + GenImageCallbackInput genImageCallbackInput = new GenImageCallbackInput(); + genImageCallbackInput.setThirdTaskId(taskId); + genImageCallbackInput.setPrompt(prompt); + genImageCallbackInput.setImageUrl(genImageUrl); + genImageCallbackInput.setStatus(status); + genImageCallbackInput.setFailReason(failReason); + genImageCallbackInput.setAsyncTaskId(asyncTaskId); + genImageCallbackInput.setType(1); + genImageTaskService.genImageCallback(genImageCallbackInput); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/VoiceCallDeductionHandler.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/VoiceCallDeductionHandler.java new file mode 100644 index 0000000..c87b52b --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/VoiceCallDeductionHandler.java @@ -0,0 +1,135 @@ +package com.sonic.cow.event.inner.handler; + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.Maps; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.domain.entity.VoiceChatRecord; +import com.sonic.cow.domain.input.VoiceChatOptInput; +import com.sonic.cow.enums.DeductionTypeEnum; +import com.sonic.cow.enums.VoiceChatOptTypeEnum; +import com.sonic.cow.event.inner.EventType; +import com.sonic.cow.event.inner.payload.VoiceCallDeductionPayload; +import com.sonic.cow.event.outer.payload.UserDeductionStatPayload; +import com.sonic.cow.service.CommonSendMqService; +import com.sonic.cow.service.VoiceChatRecordService; +import com.sonic.cow.service.VoiceChatService; +import com.sonic.cow.service.VoiceService; +import com.sonic.cow.utils.DateUtils; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.enums.ImMessageTypeEnum; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.sonic.cow.utils.Constant.VOICE_CALL_DEDUCTION_AMOUNT; + +/** + * @author chenjun + */ +@Slf4j +@Component +public class VoiceCallDeductionHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private VoiceChatRecordService voiceChatRecordService; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private VoiceService voiceService; + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private VoiceChatService voiceChatService; + @Autowired + private VoiceCallEmotionScoreHandler voiceCallEmotionScoreHandler; + + @Override + public void onEvent(Event event) { + VoiceCallDeductionPayload payload = event.normalizedData(VoiceCallDeductionPayload.class); + log.info("VoiceCallDeductionHandler payload:{}", payload); + Long voiceCallId = payload.getVoiceCallId(); + //查询本次语音通话信息 + VoiceChatRecord voiceChatRecord = voiceChatRecordService.getById(voiceCallId); + log.info("VoiceCallDeductionHandler voiceChatRecord:{}", voiceChatRecord); + //不存在或者通话已经结束,则快速返回 + if (voiceChatRecord == null || voiceChatRecord.getStatus() == 3) { + return; + } +// //用户离开,则不发送扣费,判断元素是否存在 +// Double score = stringRedisTemplate.opsForZSet().score(redisKeyUtils.voiceChatUserLeaveRoomKey(), payload.getUserId().toString()); +// log.info("VoiceCallDeductionHandler score:{}", score); +// if (score != null) { +// return; +// } + //如果余额不足,发送IM消息并发起结算 + boolean balanceIsInsufficient = voiceService.checkBalanceIsInsufficient(payload.getUserId(), VOICE_CALL_DEDUCTION_AMOUNT); + if (balanceIsInsufficient) { + //发送IM消息,通知前端 + Map extra = new HashMap<>(); + extra.put("type", ImMessageTypeEnum.INSUFFICIENT_BALANCE.name()); + extra.put("content", "INSUFFICIENT_BALANCE"); + SendAiCustomerMessageInput sendAiCustomerMessageInput = SendAiCustomerMessageInput.builder() + .fromUserId(payload.getUserId()) + .toUserId(payload.getAiId()) + .content("INSUFFICIENT_BALANCE") + .attachment(JSON.toJSONString(extra)) + .build(); + imMessageClient.sendUserToAiCustomerMessage(sendAiCustomerMessageInput); + //余额不足,发送MQ,余额不足处理 +// commonSendMqService.userBalanceInsufficientCheckoutMq(payload.getUserId()); + //调用stopVoiceChat接口,停止语音通话 + Long startTime = voiceChatRecord.getCreateTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + Long duration = (System.currentTimeMillis() - startTime); + VoiceChatOptInput voiceChatOptInput = VoiceChatOptInput.builder() + .aiId(voiceChatRecord.getAiId()) + .roomId(voiceChatRecord.getRoomId()) + .optType(VoiceChatOptTypeEnum.STOP) + .taskId(voiceChatRecord.getTaskId()) + .duration(duration) + .build(); + log.info("VoiceCallDeductionHandler voiceChatOptInput:{} ", JSON.toJSONString(voiceChatOptInput)); + voiceChatService.voiceChatOpt(voiceChatRecord.getUserId(), voiceChatOptInput); + return; + } + //发送到用户预扣费统计队列 + HashMap extraMap = Maps.newHashMap(); + extraMap.put("voiceCallId", voiceCallId); + //当前时间-开始时间 + Long duration = DateUtils.getTimeDifference(voiceChatRecord.getCreateTime(), LocalDateTime.now()); + extraMap.put("duration", duration); + extraMap.put("status", voiceChatRecord.getStatus()); + commonSendMqService.userDeductionStatSendMq(UserDeductionStatPayload.builder() + .userId(payload.getUserId()) + .aiId(payload.getAiId()) + .deductionType(DeductionTypeEnum.VOICE_CALL) + .bizTime(LocalDateTime.now()) + .extra(JSON.toJSONString(extraMap)) + .build()); + //发送到死信队列,1分钟后再执行 + commonSendMqService.voiceCallDeductionSendDeadMq(payload.getUserId(), payload.getAiId(), payload.getVoiceCallId()); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.VOICE_CALL_DEDUCTION.getEventCode(), this); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/VoiceCallEmotionScoreHandler.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/VoiceCallEmotionScoreHandler.java new file mode 100644 index 0000000..9dfd6ed --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/VoiceCallEmotionScoreHandler.java @@ -0,0 +1,190 @@ +package com.sonic.cow.event.inner.handler; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessage; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import com.google.common.collect.Lists; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.cow.client.ResponseChatClient; +import com.sonic.cow.client.request.ContentRequest; +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.client.response.ResponseChatResponse; +import com.sonic.cow.event.inner.EventType; +import com.sonic.cow.event.inner.payload.EmotionScorePayload; +import com.sonic.cow.event.outer.payload.AiChatPayload; +import com.sonic.cow.event.outer.payload.CalcHeartbeatLevelPayload; +import com.sonic.cow.service.CommonSendMqService; +import com.sonic.cow.service.ContextSessionCacheService; +import com.sonic.cow.service.InputRequestBuildService; +import com.sonic.cow.service.VoiceChatService; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.pigeon.lib.bo.ScoreExtension; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.List; + + +/** + * @author chenjun + */ +@Slf4j +@Component +public class VoiceCallEmotionScoreHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private ContextSessionCacheService contextSessionCacheService; + @Autowired + private InputRequestBuildService inputRequestBuildService; + @Autowired + private ResponseChatClient responseChatClient; + @Autowired + private VoiceChatService voiceChatService; + + @Override + public void onEvent(Event event) { + EmotionScorePayload payload = event.normalizedData(EmotionScorePayload.class); + log.info("EmotionScoreHandler payload:{}", payload); + Long userId = payload.getUserId(); + Long aiId = payload.getAiId(); + List chatMessageList = payload.getChatMessageList(); + //对话消息要成对才执行情绪打分 + if (CollectionUtils.isNotEmpty(chatMessageList) && chatMessageList.size() > 1) { + BigDecimal voiceCallEmotionScore = getVoiceCallEmotionScore(userId, aiId, chatMessageList); + + if (voiceCallEmotionScore != null) { + log.info("===>EmotionScoreHandler score: {}", voiceCallEmotionScore); + //发送MQ进行等级结算 + commonSendMqService.calHeartbeatLevelSendMq(CalcHeartbeatLevelPayload.builder() + .userId(userId) + .aiId(aiId) + .heartbeatVal(voiceCallEmotionScore) + .type(CalcHeartbeatLevelPayload.Type.VOICE_CHAT) + .build()); + //发送IM消息更新心动值 + SendAiCustomerMessageInput sendAiCustomerMessageInput = new SendAiCustomerMessageInput(); + sendAiCustomerMessageInput.setFromUserId(aiId); + sendAiCustomerMessageInput.setToUserId(userId); + sendAiCustomerMessageInput.setContent("语音通话"); + sendAiCustomerMessageInput.setAttachment(JSONObject.toJSONString(ScoreExtension.builder().score(voiceCallEmotionScore).build())); + sendAiCustomerMessageInput.setExtension(JSONObject.toJSONString(ScoreExtension.builder().score(voiceCallEmotionScore).build())); + imMessageClient.sendAiToUserCustomerMessage(sendAiCustomerMessageInput); + } + } + Boolean isEnd = payload.getIsEnd(); + if (!isEnd) { + //缓存每轮对话内容 + cacheAndGetVoiceChatInputRequests(userId, aiId, chatMessageList, isEnd); + } else { + //通话结束时,获取缓存中本次语音通话所有内容 + List voiceChatInputRequestList = cacheAndGetVoiceChatInputRequests(userId, aiId, chatMessageList, isEnd); + log.info("===>EmotionScoreHandler voiceChatInputRequestList:{}", JSON.toJSONString(voiceChatInputRequestList)); + //语音通话结束处理 + voiceChatService.voiceCallEndHandler(userId, aiId, voiceChatInputRequestList, payload.getDuration()); + } + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.EMOTION_SCORE.getEventCode(), this); + } + + /** + * 语音通话情绪打分 + * + * @param fromUserId + * @param toUserId + * @return + */ + private BigDecimal getVoiceCallEmotionScore(Long fromUserId, Long toUserId, List messages) { + //获取上下文缓存 这个缓存一定是存在的 + String responseId = contextSessionCacheService.getByRedis(fromUserId, toUserId); + List inputRequests = Lists.newArrayList(); + if (StringUtils.isEmpty(responseId)) { + AiChatPayload payload = AiChatPayload.builder().fromUserId(fromUserId).toUserId(toUserId).build(); + //调用公共方法,组装入参数据 + inputRequestBuildService.buildV2(payload, inputRequests, 30, false); + } + //构造系统提示词 + String emotionScorePrompt = inputRequestBuildService.buildVoiceCallEmotionScorePromptContent(JSON.toJSONString(messages)); + inputRequests.add(InputRequest.builder() + .role(ChatMessageRole.SYSTEM) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(emotionScorePrompt).build())).build()); + //执行聊天 不将添加缓存 + ResponseChatResponse chatResponse = responseChatClient.responseStructuredChat(responseId, null, inputRequests, null, false); + log.info("===> getVoiceCallEmotionScore chatResponse : {}", chatResponse); + if (chatResponse != null && chatResponse.getScore() != null) { + return chatResponse.getScore(); + } + return BigDecimal.ZERO; + } + + private List cacheAndGetVoiceChatInputRequests(Long userId, Long aiId, List messages, Boolean isEnd) { + //不是成对的,不记录 + if (messages.size() % 2 != 0) { + return null; + } + List inputRequestList = Lists.newArrayList(); + String voiceChatMessageCacheKey = redisKeyUtils.voiceChatMessageCacheKey(userId, aiId); + String voiceChatMessageStr = stringRedisTemplate.opsForValue().get(voiceChatMessageCacheKey); + for (ChatMessage message : messages) { + inputRequestList.add(InputRequest.builder() + .type("message") + .role(message.getRole()) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(message.getContent().toString()).build())) + .build()); + } + if (StringUtils.isNotEmpty(voiceChatMessageStr)) { + //之前有的话,先拿出来,再添加放到缓存中 + List oldInputRequestList = JSON.parseArray(voiceChatMessageStr, InputRequest.class); + oldInputRequestList.addAll(inputRequestList); + //赋值到新的 + inputRequestList = oldInputRequestList; + } + if (!isEnd) { + //通话未结束,要缓存 + stringRedisTemplate.opsForValue().set(voiceChatMessageCacheKey, JSON.toJSONString(inputRequestList)); + } else { + //通话结束,要删除 + stringRedisTemplate.delete(voiceChatMessageCacheKey); + } + return inputRequestList; + } + + /** + * 余额不足时,获取语音通话中的聊天内容 + * @param userId + * @param aiId + * @return + */ + public List getVoiceChatInputRequests(Long userId, Long aiId) { + List inputRequestList = Lists.newArrayList(); + String voiceChatMessageCacheKey = redisKeyUtils.voiceChatMessageCacheKey(userId, aiId); + String voiceChatMessageStr = stringRedisTemplate.opsForValue().get(voiceChatMessageCacheKey); + if (StringUtils.isNotEmpty(voiceChatMessageStr)) { + return JSON.parseArray(voiceChatMessageStr, InputRequest.class); + } + return inputRequestList; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/VoiceCallWebhookHandler.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/VoiceCallWebhookHandler.java new file mode 100644 index 0000000..df00b83 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/handler/VoiceCallWebhookHandler.java @@ -0,0 +1,165 @@ +package com.sonic.cow.event.inner.handler; + +import com.alibaba.fastjson.JSON; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.cow.client.rtc.EventData; +import com.sonic.cow.domain.entity.VoiceChatRecord; +import com.sonic.cow.domain.input.VoiceChatOptInput; +import com.sonic.cow.enums.VoiceChatOptTypeEnum; +import com.sonic.cow.event.inner.EventType; +import com.sonic.cow.event.inner.payload.VoiceCallWebhookPayload; +import com.sonic.cow.service.CommonSendMqService; +import com.sonic.cow.service.VoiceChatRecordService; +import com.sonic.cow.service.VoiceChatService; +import com.sonic.cow.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.concurrent.TimeUnit; + +/** + * @author chenjun + */ +@Slf4j +@Component +public class VoiceCallWebhookHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private VoiceChatService voiceChatService; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private VoiceChatRecordService voiceChatRecordService; + + @Override + public void onEvent(Event event) { + try { + VoiceCallWebhookPayload payload = event.normalizedData(VoiceCallWebhookPayload.class); + log.info("VoiceCallWebhookHandler payload:{}", payload); + com.sonic.cow.client.rtc.Event voiceCallEvent = payload.getVoiceCallEvent(); + String eventType = voiceCallEvent.getEventType(); + //房间销毁 + if ("RoomDestroy".equals(eventType)) { + //房间销毁,调用stopVoiceChat接口,停止语音通话 + roomDestroy(voiceCallEvent); + } else if ("UserLeaveRoom".equals(eventType)) { + userLeaveRoom(voiceCallEvent); + } else if ("UserJoinRoom".equals(eventType)) { + //从redis zset中移除元素 + userJoinRoom(voiceCallEvent); + } + } catch (Exception e) { + log.error("VoiceCallWebhookHandler error:", e); + } + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.VOICE_CALL_WEBHOOK.getEventCode(), this); + } + + /** + * 房间销毁处理 + * + * @param voiceCallEvent + */ + private void roomDestroy(com.sonic.cow.client.rtc.Event voiceCallEvent) { + EventData eventData = JSON.parseObject(voiceCallEvent.getEventData(), EventData.class); + String roomId = eventData.getRoomId(); + //获得通话双方的用户id + String[] split = roomId.split("-"); + Long userId = Long.valueOf(split[0]); + Long aiId = Long.valueOf(split[1]); + + //缓存中获取到taskId + String taskId = stringRedisTemplate.opsForValue().get(redisKeyUtils.voiceChatTaskIdKey(userId.toString())); + log.info("roomDestroy taskId:{}", taskId); + //如果删除了,则直接返回 + if (taskId == null) { + return; + } + //查询聊天记录 + VoiceChatRecord voiceChatRecord = voiceChatRecordService.getByRoomIdAndTaskId(roomId, taskId); + //不存在聊天记录,return + if (voiceChatRecord == null) { + return; + } + //已经结束return + if (voiceChatRecord.getStatus() == 3) { + return; + } + //时长 + LocalDateTime createTime = voiceChatRecord.getCreateTime(); + Long startTime = createTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + Long duration = (System.currentTimeMillis() - startTime); + //调用stopVoiceChat接口,停止语音通话 + VoiceChatOptInput voiceChatOptInput = VoiceChatOptInput.builder() + .aiId(aiId) + .roomId(roomId) + .optType(VoiceChatOptTypeEnum.STOP) + .taskId(taskId) + .duration(duration) + .build(); + log.info("VoiceCallWebhookHandler voiceChatOptInput:{} ", JSON.toJSONString(voiceChatOptInput)); + voiceChatService.voiceChatOpt(userId, voiceChatOptInput); + } + + /** + * 用户离开房间处理 + * + * @param voiceCallEvent + */ + private void userLeaveRoom(com.sonic.cow.client.rtc.Event voiceCallEvent) { + EventData eventData = JSON.parseObject(voiceCallEvent.getEventData(), EventData.class); + String userId = eventData.getUserId(); + //真实用户离开,停止费用计算,定时任务,如果超过3分钟没有加入,调用stopVoiceChat接口,停止语音通话 + if (!userId.contains("@")) { + stringRedisTemplate.opsForZSet().add(redisKeyUtils.voiceChatUserLeaveRoomKey(), userId, System.currentTimeMillis()); + } + } + + + /** + * 用户加入房间处理 + * + * @param voiceCallEvent + */ + private void userJoinRoom(com.sonic.cow.client.rtc.Event voiceCallEvent) { + EventData eventData = JSON.parseObject(voiceCallEvent.getEventData(), EventData.class); + String userId = eventData.getUserId(); + String roomId = eventData.getRoomId(); + //真实用户加入,删除redis + if (!userId.contains("@")) { + //用户离开房间中移除 + stringRedisTemplate.opsForZSet().remove(redisKeyUtils.voiceChatUserLeaveRoomKey(), userId); + //获取最近一个未关闭的语音通话 + String taskId = stringRedisTemplate.opsForValue().get(redisKeyUtils.voiceChatTaskIdKey(userId)); + if (taskId == null) { + return; + } + VoiceChatRecord voiceChatRecord = voiceChatRecordService.getByRoomIdAndTaskId(roomId, taskId); + //不存在,return + if (voiceChatRecord == null) { + return; + } + //已经结束return + if (voiceChatRecord.getStatus() == 3) { + return; + } + } + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/AiGenImagePayload.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/AiGenImagePayload.java new file mode 100644 index 0000000..503da89 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/AiGenImagePayload.java @@ -0,0 +1,30 @@ +package com.sonic.cow.event.inner.payload; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author chenjun + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiGenImagePayload { + + @ApiModelProperty("批次号") + private String batchNo; + + @ApiModelProperty("任务id") + private String taskId; + + @ApiModelProperty("换脸的图片url(基图,首次创建AI,选择的形象图,创作相册图片时用到)") + private String imageUrl; + + @ApiModelProperty("生成图片的prompt") + private String prompt; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/EmotionScorePayload.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/EmotionScorePayload.java new file mode 100644 index 0000000..3ab60ed --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/EmotionScorePayload.java @@ -0,0 +1,36 @@ +package com.sonic.cow.event.inner.payload; + +import com.byteplus.ark.runtime.model.completion.chat.ChatMessage; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @author mzc + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EmotionScorePayload { + + @ApiModelProperty("聊天消息列表") + private List chatMessageList; + + @ApiModelProperty("用户id") + private Long userId; + + @ApiModelProperty("AI id") + private Long aiId; + + @ApiModelProperty("通话是否结束") + private Boolean isEnd; + + @ApiModelProperty("通话结束时才有,通话时长") + private Long duration; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/VoiceCallDeductionPayload.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/VoiceCallDeductionPayload.java new file mode 100644 index 0000000..f542529 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/VoiceCallDeductionPayload.java @@ -0,0 +1,33 @@ +package com.sonic.cow.event.inner.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author mzc + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VoiceCallDeductionPayload { + + /** + * 用户ID + */ + private Long userId; + + /** + * AI ID + */ + private Long aiId; + + /** + * voice_chat_record表主键id + */ + private Long voiceCallId; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/VoiceCallWebhookPayload.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/VoiceCallWebhookPayload.java new file mode 100644 index 0000000..a075214 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/inner/payload/VoiceCallWebhookPayload.java @@ -0,0 +1,24 @@ +package com.sonic.cow.event.inner.payload; + +import com.sonic.cow.client.rtc.Event; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author mzc + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VoiceCallWebhookPayload { + + /** + * 用户ID + */ + private Event voiceCallEvent; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/EventType.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/EventType.java new file mode 100644 index 0000000..61a0560 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/EventType.java @@ -0,0 +1,48 @@ +package com.sonic.cow.event.outer; + +import com.sonic.common.event.Event; +import com.sonic.cow.config.EventConfig; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义不属于当前系统的消息 + * @author chenjun + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** 事件定义 */ + USER_CREATED(Event.BuildInScene.BS.getCode(), "bs_user", "user_created", "用户创建"), + + /** + * 和AI聊天 + */ + AI_CHAT(EventConfig.DEFAULT_SCENE, EventConfig.PIGEON, "ai_chat", "和AI聊天"), + + /** + * 计算心动等级 + */ + CALC_HEARTBEAT_LEVEL(EventConfig.DEFAULT_SCENE, EventConfig.FROG, "calc_heartbeat_level", "心动等级计算"), + + /** + * 用户文本,语音,语音通话预扣款统计 + */ + USER_DEDUCTION_STAT(EventConfig.DEFAULT_SCENE, EventConfig.FROG, "user_deduction_stat", "文本,语音,语音通话预扣款统计"), + + USER_BALANCE_INSUFFICIENT_CHECKOUT(EventConfig.DEFAULT_SCENE, EventConfig.FROG, "user_balance_insufficient_checkout", "余额不足,文本,语音,语音通话预扣款结算"), + + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/handler/AiChatV2Handler.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/handler/AiChatV2Handler.java new file mode 100644 index 0000000..2c445cc --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/handler/AiChatV2Handler.java @@ -0,0 +1,58 @@ +package com.sonic.cow.event.outer.handler; + +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.common.utils.RedisLock; +import com.sonic.cow.event.outer.EventType; +import com.sonic.cow.event.outer.payload.AiChatPayload; +import com.sonic.cow.service.ChatHandlerService; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.pigeon.lib.bo.AttachBo; +import com.sonic.pigeon.lib.enums.MessageTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author chenjun + */ +@Slf4j +@Component +public class AiChatV2Handler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private ChatHandlerService chatHandlerService; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + + @Override + public void onEvent(Event event) { + //执行AI聊天的逻辑 + AiChatPayload payload = event.normalizedData(AiChatPayload.class); + //非自定义图片消息的话直接给拒绝掉 + AttachBo attachBo = StringUtils.isEmpty(payload.getAttach()) ? null : JSONObject.parseObject(payload.getAttach(), AttachBo.class); + if(MessageTypeEnum.CUSTOM == payload.getMessageType() && (attachBo == null || !"IMAGE".equalsIgnoreCase(attachBo.getType()))) { + return; + } + //加锁处理,防止并发访问 + RedisLock redisLock = new RedisLock(redisKeyUtils.aiChatLockKey(payload.getFromUserId(), payload.getToUserId()), redisWrapper); + redisLock.tryAcquireRun(60 * 1000, 60 * 1000, () -> { + chatHandlerService.handle(payload, attachBo); + return true; + }); + } + + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.AI_CHAT.getEventCode(), this); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/handler/UserCreatedThenHandler.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/handler/UserCreatedThenHandler.java new file mode 100644 index 0000000..d3e7a77 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/handler/UserCreatedThenHandler.java @@ -0,0 +1,33 @@ +package com.sonic.cow.event.outer.handler; + +import com.sonic.cow.event.outer.EventType; +import com.sonic.cow.event.outer.payload.UserCratedPayload; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author chenjun + */ +@Slf4j +@Component +public class UserCreatedThenHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + + @Override + public void onEvent(Event event) { + UserCratedPayload payload = event.normalizedData(UserCratedPayload.class); + // TODO: + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.USER_CREATED.getEventCode(), this); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/AiChatPayload.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/AiChatPayload.java new file mode 100644 index 0000000..12ea730 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/AiChatPayload.java @@ -0,0 +1,45 @@ +package com.sonic.cow.event.outer.payload; + +import com.sonic.pigeon.lib.enums.MessageTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author coder + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiChatPayload { + + private Long fromUserId; + + private Long toUserId; + + private String content; + + /** + * 消息类型 + */ + private MessageTypeEnum messageType; + + /** 消息附件 */ + private String attach; + + /** + * 消息来源 + */ + private SourceType sourceType = SourceType.IM; + + + public enum SourceType { + IM, + GIFT; + + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/CalcHeartbeatLevelPayload.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/CalcHeartbeatLevelPayload.java new file mode 100644 index 0000000..ed58043 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/CalcHeartbeatLevelPayload.java @@ -0,0 +1,63 @@ +package com.sonic.cow.event.outer.payload; + +import com.google.common.collect.Lists; +import lombok.*; + +import java.math.BigDecimal; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class CalcHeartbeatLevelPayload { + /** + * 用户id + */ + private Long userId; + + /** + * AI的id + */ + private Long aiId; + + /** + * 心动值 + */ + private BigDecimal heartbeatVal; + + /** + * 类型 聊天,24小时未聊天扣减心动值,发送礼物,语音通话 + */ + private Type type; + + /** + * 扩展字段 + */ + private String ext; + + @Getter + public enum Type { + //聊天 + CHAT, + //24小时未聊天扣减心动值 + HOURS_WITHOUT_CHAT, + //发送礼物 + SEND_GIFT, + //语音通话 + VOICE_CHAT, + ; + + /** + * 是否聊天类型 + * + * @param type + * @return + */ + public static Boolean isChatType(Type type) { + List chatTypeList = Lists.newArrayList(CHAT, SEND_GIFT, VOICE_CHAT); + return chatTypeList.contains(type); + } + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/UserBalanceInsufficientCheckoutPayload.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/UserBalanceInsufficientCheckoutPayload.java new file mode 100644 index 0000000..5081069 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/UserBalanceInsufficientCheckoutPayload.java @@ -0,0 +1,22 @@ +package com.sonic.cow.event.outer.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author coder + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserBalanceInsufficientCheckoutPayload { + + /** + * 用户id + */ + private Long userId; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/UserCratedPayload.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/UserCratedPayload.java new file mode 100644 index 0000000..ef18ad9 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/UserCratedPayload.java @@ -0,0 +1,18 @@ +package com.sonic.cow.event.outer.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author chenjun + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserCratedPayload { + private Long userId; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/UserDeductionStatPayload.java b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/UserDeductionStatPayload.java new file mode 100644 index 0000000..dbf08c1 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/event/outer/payload/UserDeductionStatPayload.java @@ -0,0 +1,48 @@ +package com.sonic.cow.event.outer.payload; + +import com.sonic.cow.enums.DeductionTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @author coder + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserDeductionStatPayload { + + /** + * 用户id + */ + private Long userId; + + /** + * AI id + */ + private Long aiId; + + /** + * 扣除类型 + */ + private DeductionTypeEnum deductionType; + + /** + * 业务发生时间 + */ + private LocalDateTime bizTime; + + /** + * 额外扩展字段 json + * 文本:用户内容,ai内容 ["userContent":"xxxx","aiContent":"xxx"}] + * 语音:发送语音 + * 语音通话:是否结束 {"status":1} 1: 通话中,2:通话结束 + */ + private String extra; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/job/AutoSendAiMessageJobV2Handler.java b/sonic-cow/server/src/main/java/com/sonic/cow/job/AutoSendAiMessageJobV2Handler.java new file mode 100644 index 0000000..fc4f99d --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/job/AutoSendAiMessageJobV2Handler.java @@ -0,0 +1,137 @@ +package com.sonic.cow.job; + + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import com.google.common.collect.Lists; +import com.sonic.bear.lib.client.UserNicknamePoolClient; +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.cow.client.ResponseChatClient; +import com.sonic.cow.client.request.ContentRequest; +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.client.response.ResponseChatResponse; +import com.sonic.cow.domain.entity.PromptConfig; +import com.sonic.cow.enums.PromptTypeEnum; +import com.sonic.cow.service.ContextSessionCacheService; +import com.sonic.cow.service.PromptConfigService; +import com.sonic.cow.service.impl.InputRequestBuildServiceImpl; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.input.SendAiTextMessageInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + + +/** + * 超过24小时未聊天时,AI自动给用户发送一条消息 + * @author code + */ +@Slf4j +@Component +public class AutoSendAiMessageJobV2Handler { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private ResponseChatClient responseChatClient; + @Autowired + private ContextSessionCacheService contextSessionCacheService; + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private PromptConfigService promptConfigService; + @Autowired + private UserNicknamePoolClient userNicknamePoolClient; + + + /** 每5分钟行一次 */ + @Scheduled(cron = "0 */5 * * * *") + public void autoSendAiMessageScanSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("autoSendAiMessageScanJob", TimeUnit.MINUTES.toSeconds(1), this::autoSendAiMessageScanJob); + } + + protected JobmanClient.JobResult autoSendAiMessageScanJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> autoSendAiMessageScanJob start !!!"); + long startTime = System.currentTimeMillis() / 1000 - 3 * 24 * 60 * 60; + long endTime = System.currentTimeMillis() / 1000 - 24 * 60 * 60; + Set> redisResults = stringRedisTemplate.opsForZSet() + .rangeByScoreWithScores(redisKeyUtils.notifyUserZSetKey(), startTime, endTime); + List allList = Lists.newArrayList(); + for (ZSetOperations.TypedTuple tuple : redisResults) { + //获取 value + allList.add(tuple.getValue()); + } + //没有数据则不继续执行 + if(CollectionUtils.isEmpty(allList)) { + return JobmanClient.JobResult.success(0); + } + log.info("===> autoSendAiMessageScanJob list : {}", allList); + //数据库查询模板数据 + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.AUTO_SEND_CHAT_SYSTEM_PROMPT_TEMPLATE.name()); + for (String ftUserId : allList) { + String[] split = ftUserId.split("-"); + Long fromUserId = Long.parseLong(split[0]); + Long toUserId = Long.parseLong(split[1]); + //获取上下文缓存 这个缓存一定是存在的 + String responseId = contextSessionCacheService.getByRedis(fromUserId, toUserId); + //批量查询昵称 + Map nicknameMap = userNicknamePoolClient.batchGetNickname(Lists.newArrayList(fromUserId, toUserId)); + log.debug("===> nicknameMap : {}", nicknameMap); + Map param = new HashMap<>(2); + param.put("userNickname", nicknameMap.get(fromUserId)); + param.put("aiNickname", nicknameMap.get(toUserId)); + //处理系统提示词 + String systemPromptConfigContent = InputRequestBuildServiceImpl.processTemplate(systemPromptConfig.getPromptTemplate(), param); + + //构造系统提示词 + List inputRequests = Lists.newArrayList(InputRequest.builder() + .role(ChatMessageRole.SYSTEM) + .content(Lists.newArrayList(ContentRequest.builder() + .type("input_text") + .text(systemPromptConfigContent) + .build())).build()); + //执行聊天 不将添加缓存 + ResponseChatResponse chatResponse = responseChatClient.responseStructuredChat(responseId, null, inputRequests, null, false); + log.info("===> chatResponse : {}", chatResponse); + if(chatResponse == null) { + //删除数据 + stringRedisTemplate.opsForZSet().remove(redisKeyUtils.notifyUserZSetKey(), ftUserId); + continue; + } + log.info("===> AiChatHandler chat time : {} ms", System.currentTimeMillis() - startTime); + // 发送消息给用户 + SendAiTextMessageInput sendAiTextMessageInput = new SendAiTextMessageInput(); + sendAiTextMessageInput.setFromUserId(toUserId); + sendAiTextMessageInput.setToUserId(fromUserId); + sendAiTextMessageInput.setContent(chatResponse.getMessage()); + imMessageClient.sendAiToUserTextMessage(sendAiTextMessageInput); + //删除数据 + stringRedisTemplate.opsForZSet().remove(redisKeyUtils.notifyUserZSetKey(), ftUserId); + } + + int count = allList.size(); + log.info("===> autoSendAiMessageScanJob count : {} end !!!", count); + return JobmanClient.JobResult.success(count); + } finally { + LogUtils.removeTraceId(); + } + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/job/GenImageHeartBeatScanJobHandler.java b/sonic-cow/server/src/main/java/com/sonic/cow/job/GenImageHeartBeatScanJobHandler.java new file mode 100644 index 0000000..0dd1237 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/job/GenImageHeartBeatScanJobHandler.java @@ -0,0 +1,45 @@ +package com.sonic.cow.job; + + +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.cow.service.GenImageTaskService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 生成图片任务前端心跳过期数据扫描 + * @author mzc + */ +@Slf4j +@Component +public class GenImageHeartBeatScanJobHandler { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private GenImageTaskService genImageTaskService; + + /** 每10秒执行一次 */ + @Scheduled(cron = "0/10 * * * * ? ") + public void genImageHeartBeatScanSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("genImageHeartBeatScanJob", TimeUnit.MINUTES.toSeconds(2), this::genImageHeartBeatScanJob); + } + + protected JobmanClient.JobResult genImageHeartBeatScanJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> genImageHeartBeatScanJob start !!!"); + int count = genImageTaskService.scanHeartBeatExpTime(); + log.info("===> genImageHeartBeatScanJob count : {} end !!!", count); + return JobmanClient.JobResult.success(count); + } finally { + LogUtils.removeTraceId(); + } + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/job/VoiceChatUserLeaveHandler.java b/sonic-cow/server/src/main/java/com/sonic/cow/job/VoiceChatUserLeaveHandler.java new file mode 100644 index 0000000..de02725 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/job/VoiceChatUserLeaveHandler.java @@ -0,0 +1,88 @@ +package com.sonic.cow.job; + + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.cow.service.VoiceChatService; +import com.sonic.cow.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + + +/** + * 用户离开3分钟后,如果语音通话未结束,停止语音通话 + * + * @author code + */ +@Slf4j +@Component +public class VoiceChatUserLeaveHandler { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private VoiceChatService voiceChatService; + + /** + * 用户离开3分钟后,如果语音通话未结束,停止语音通话 + */ + private static final Long LEAVE_TIME = 1 * 60 * 1000L; + + /** + * 每1分钟行一次 + */ + @Scheduled(cron = "0 */1 * * * *") + public void voiceChatUserLeaveSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("voiceChatUserLeaveJob", TimeUnit.MINUTES.toSeconds(1), this::voiceChatUserLeaveJob); + } + + protected JobmanClient.JobResult voiceChatUserLeaveJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + String voiceChatUserLeaveRoomKey = redisKeyUtils.voiceChatUserLeaveRoomKey(); + Set> typedTuples = stringRedisTemplate.opsForZSet().rangeWithScores(voiceChatUserLeaveRoomKey, 0, -1); + if (CollectionUtils.isNotEmpty(typedTuples)) { + for (ZSetOperations.TypedTuple typedTuple : typedTuples) { + //score为离开时的时间戮 + Double score = typedTuple.getScore(); + //value为用户id + String userId = typedTuple.getValue(); + log.info("voiceChatUserLeaveJob userId:{}", userId); + if (StringUtils.isEmpty(userId) || score == null) { + continue; + } + //离开的时间戮 + Long timestamp = score.longValue(); + if (Long.valueOf(timestamp) + LEAVE_TIME > System.currentTimeMillis()) { + continue; + } + Boolean isStop = voiceChatService.stopVoiceChat(Long.valueOf(userId)); + if (isStop) { + //清理掉已处理的redis数据 + stringRedisTemplate.opsForZSet().remove(voiceChatUserLeaveRoomKey, userId); + } + } + } + return JobmanClient.JobResult.success(0); + } catch (Exception e) { + log.error("voiceChatUserLeaveJob error", e); + } finally { + LogUtils.removeTraceId(); + } + return JobmanClient.JobResult.success(0); + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/limit/RequestLimit.java b/sonic-cow/server/src/main/java/com/sonic/cow/limit/RequestLimit.java new file mode 100644 index 0000000..6825e9a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/limit/RequestLimit.java @@ -0,0 +1,44 @@ +package com.sonic.cow.limit; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.*; + +/** + * 限流注解 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +@Order(Ordered.HIGHEST_PRECEDENCE) +public @interface RequestLimit { + + /** + * 允许访问的最大次数 + */ + int count() default Integer.MAX_VALUE; + + /** + * 已登录用户允许访问的最大次数(备注:如果不配置值的话默认都走IP的限制) + * @return + */ + int loginCount() default Integer.MAX_VALUE; + + /** + * 时间段,单位为毫秒,默认值一分钟 + */ + long time() default 60000; + + /** + * 未登录请求用户达到限流时的提示 异常码 默认值为:1001009 / 如果想要未登录用户跳转到登录页面则返回异常码为 noLoginErrorCode = "10050001" + * @return + */ + String noLoginErrorCode() default "1001009"; + + /** + * message 提示文案 + * @return + */ + String message() default "Sorry to detect your abnormal access, please try again later"; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/limit/RequestLimitContract.java b/sonic-cow/server/src/main/java/com/sonic/cow/limit/RequestLimitContract.java new file mode 100644 index 0000000..1f3ea0a --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/limit/RequestLimitContract.java @@ -0,0 +1,109 @@ +package com.sonic.cow.limit; + + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.enums.AppEnv; +import com.sonic.common.utils.IpAddressUtils; +import com.sonic.cow.enums.BizResultCode; +import com.sonic.cow.enums.BusinessException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 限流控制器 + */ +@Order(99) +@Slf4j +@Aspect +@Component +public class RequestLimitContract { + + @Value("${spring.profiles.active}") + private String runMode; + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * redis key不存在的过期时间值 + */ + private final Integer NULL_KEY_EXP_TIME_VALUE = -2; + + /** + * redis key永不过期的过期时间值 + */ + private final Integer NEVER_EXPIRES_VALUE = -1; + + /** + * 默认的异常码 + */ + private static final String DEFAULT_ERROR_CODE = "1999999"; + + @Before("execution(public * com.sonic.cow.controller..*.*(..)) && @annotation(limit)") + public void requestLimit(final JoinPoint joinPoint, RequestLimit limit) { + //dev环境直接放行不做拦截 + if (StringUtils.isNotBlank(runMode) && AppEnv.dev.name().equals(runMode)) { + return; + } + Object[] args = joinPoint.getArgs(); + HttpServletRequest request = null; + Session session = null; + for (Object arg : args) { + //解析方法的HttpServletRequest入参对象 + if (arg instanceof HttpServletRequest) { + request = (HttpServletRequest) arg; + } + //解析方法的Session入参对象、必须要配置了登录次数限制的才能进行解析 + if (arg instanceof Session && limit.loginCount() != Integer.MAX_VALUE) { + session = (Session) arg; + } + } + //判断请求是否为空,是否需要抛出异常 + BizResultCode.MISS_PARAM_ERROR.check(request == null); + int num = 0; + String ipOrUserId = null; + Integer limitCount = null; + String url = null; + boolean loginUserBl = false; + try { + ipOrUserId = (session == null || session.getUserId() == null) ? IpAddressUtils.getIpAddress(request) : session.getUserId().toString(); + limitCount = (session == null || session.getUserId() == null) ? limit.count() : limit.loginCount(); + loginUserBl = session != null && session.getUserId() != null; + url = request.getRequestURI(); + //eg: limit:path:/mobile/third/login:127-0-0-1 + String key = "limit:path:".concat(url).concat(":").concat(StringUtils.isNotEmpty(ipOrUserId) ? ipOrUserId.replace(":", "-") : ipOrUserId); + //处理限流为-1的情况 + Long expTime = stringRedisTemplate.getExpire(key); + if (expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().set(key, "1", limit.time(), TimeUnit.MILLISECONDS); + } else { + num = Objects.requireNonNull(stringRedisTemplate.opsForValue().increment(key, 1)).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis的key + stringRedisTemplate.delete(key); + } + } catch (Exception e) { + log.error("requestLimit error", e); + } + if (num > limitCount) { + log.info("===> 限流触发,访问地址:{}, 用户信息:{}, 限定的次数:{}", ipOrUserId, url, limit.count()); + //未登录的用户达到限流时如果配置了跳转登录页的errorCode的话前端会直接去跳转登录页面 + throw new BusinessException(loginUserBl ? DEFAULT_ERROR_CODE : limit.noLoginErrorCode(), limit.message()); + } + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/AiGenImageService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/AiGenImageService.java new file mode 100644 index 0000000..bd79708 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/AiGenImageService.java @@ -0,0 +1,17 @@ +package com.sonic.cow.service; + +import com.sonic.cow.domain.input.AiGenImageInput; + +import java.util.List; + +public interface AiGenImageService { + + /** + * 生成形象图 + * + * @param input + * @return + */ + void aiGenImage(AiGenImageInput input, String batchNo, List taskIdList); +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/AiMessageService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/AiMessageService.java new file mode 100644 index 0000000..f285c26 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/AiMessageService.java @@ -0,0 +1,15 @@ +package com.sonic.cow.service; + +import com.sonic.cow.domain.input.AiMessageDelInput; + +public interface AiMessageService { + + /** + * ai消息删除 + * + * @param input + * @return + */ + void aiMessageDelete(AiMessageDelInput input, Long currentUserId); +} + diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/ChatHandlerService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/ChatHandlerService.java new file mode 100644 index 0000000..551d844 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/ChatHandlerService.java @@ -0,0 +1,26 @@ +package com.sonic.cow.service; + +import com.sonic.cow.client.response.ResponseChatResponse; +import com.sonic.cow.event.outer.payload.AiChatPayload; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; +import com.sonic.pigeon.lib.bo.AttachBo; + +public interface ChatHandlerService { + + /** + * 处理AiChat事件 + * @param aiChatPayload + * @param attachBo + */ + void handle(AiChatPayload aiChatPayload, AttachBo attachBo); + + + /** + * 发送自定义消息 + * @param payload + * @param message + * @param album + * @param chatResponse + */ + void sendImCustomMessage(AiChatPayload payload, String message, AIUserAlbumApiOutput album, ResponseChatResponse chatResponse); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/ChatMemoryService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/ChatMemoryService.java new file mode 100644 index 0000000..2aba2b2 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/ChatMemoryService.java @@ -0,0 +1,24 @@ +package com.sonic.cow.service; + +import com.byteplus.ark.runtime.model.completion.chat.ChatMessage; + +import java.util.List; + +public interface ChatMemoryService { + + /** + * chat聊天历史数据获取 + * @param userId + * @param aiId + */ + List getHistory(Long userId, Long aiId); + + /** + * 保存历史消息 + * @param userId + * @param aiId + * @param chatMessages + */ + void saveHistory(Long userId, Long aiId, List chatMessages); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/ChatService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/ChatService.java new file mode 100644 index 0000000..5c24bd6 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/ChatService.java @@ -0,0 +1,13 @@ +package com.sonic.cow.service; + +public interface ChatService { + + /** + * chat聊天(方法来源于sse的接口调用或im的消息回调) + * @param userId + * @param aiId + * @param message + */ + String chat(Long userId, Long aiId, String message); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/CommonSendMqService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/CommonSendMqService.java new file mode 100644 index 0000000..248bb21 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/CommonSendMqService.java @@ -0,0 +1,86 @@ +package com.sonic.cow.service; + + +import com.sonic.cow.client.rtc.Event; +import com.sonic.cow.event.inner.payload.EmotionScorePayload; +import com.sonic.cow.event.outer.payload.CalcHeartbeatLevelPayload; +import com.sonic.cow.event.outer.payload.UserDeductionStatPayload; + +/** + * @description: 发送消息到im的mq + * @author: mzc + * @create: 2025/07/15 + **/ +public interface CommonSendMqService { + + /** + * AI生成图片发送MQ-文生图 + * + * @param batchNo + * @param taskId + * @param prompt + */ + void aiTextGenImageSendMq(String batchNo, String taskId, String prompt, String imageUrl); + + /** + * AI生成图片发送MQ-图生图 + * + * @param batchNo + * @param taskId + * @param prompt + */ + void aiImageGenImageSendMq(String batchNo, String taskId, String prompt, String imageUrl); + + /** + * 计算心动等级发送mq + * + * @param payload + */ + void calHeartbeatLevelSendMq(CalcHeartbeatLevelPayload payload); + + /** + * 情绪打分发送mq + * + * @param payload + */ + void emotionScoreSendMq(EmotionScorePayload payload); + + /** + * 用户文本,语音,语音通话预扣款统计mq + * + * @param payload + */ + void userDeductionStatSendMq(UserDeductionStatPayload payload); + + /** + * 语音通话预扣费发送mq 1分钟后执行 + * + * @param userId + * @param aiId + * @param voiceCallId 语音通话id voice_chat_record表主键id + */ + void voiceCallDeductionSendDeadMq(Long userId, Long aiId, Long voiceCallId); + + /** + * 语音通话预扣费发送mq 立即执行 + * + * @param userId + * @param aiId + * @param voiceCallId 语音通话id voice_chat_record表主键id + */ + void voiceCallDeductionSendMq(Long userId, Long aiId, Long voiceCallId); + + /** + * 语音通话回调事件mq + * + * @param event + */ + void voiceCallWebhookSendMq(Event event); + + /** + * 用户余额不足,发起扣款mq + * + * @param userId + */ + void userBalanceInsufficientCheckoutMq(Long userId); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/ContextSessionCacheService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/ContextSessionCacheService.java new file mode 100644 index 0000000..1e0e9e5 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/ContextSessionCacheService.java @@ -0,0 +1,39 @@ +package com.sonic.cow.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.cow.domain.entity.ContextSessionCache; + +public interface ContextSessionCacheService extends IService { + + /** + * 获取当前有效的contextId + * @param userId + * @param toUserId + * @return + */ + String getContextId(Long userId, Long toUserId); + + /** + * 保存到redis中 + * @param fromUserId + * @param toUserId + * @param responseId + * @return + */ + void saveToRedis(Long fromUserId, Long toUserId, String responseId); + + /** + * 从 + * @param fromUserId + * @param toUserId + * @return + */ + String getByRedis(Long fromUserId, Long toUserId); + + /** + * 删除redis数据 + * @param fromUserId + * @param toUserId + */ + void deleteRedis(Long fromUserId, Long toUserId); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/FunctionCallService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/FunctionCallService.java new file mode 100644 index 0000000..8f95cdd --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/FunctionCallService.java @@ -0,0 +1,15 @@ +package com.sonic.cow.service; + +import com.sonic.cow.client.response.ResponseChatResponse; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; + +public interface FunctionCallService { + + /** + * 执行方法调用 + * @param response + * @return + */ + AIUserAlbumApiOutput callFunction(ResponseChatResponse response); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/GenAiUserContentService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/GenAiUserContentService.java new file mode 100644 index 0000000..3ec712d --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/GenAiUserContentService.java @@ -0,0 +1,29 @@ +package com.sonic.cow.service; + +import com.sonic.cow.domain.input.GenAiUserContentV1Input; +import com.sonic.cow.domain.output.GenAiUserContentV1Output; + +/** + * 一键生成AI人物基础信息 + */ +public interface GenAiUserContentService { + + /** + * 一键生成AI人物基础信息 + * + * @param input + * @return + */ + GenAiUserContentV1Output genAiUserContentV1(Long userId, GenAiUserContentV1Input input) throws Exception; + + /** + * AI 生成内容 + * + * @param promptType + * @param input + * @param clazz + * @param + * @return + */ + T genContent(String promptType, Object input, Class clazz) throws Exception; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/GenImageTaskRecordService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/GenImageTaskRecordService.java new file mode 100644 index 0000000..3289544 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/GenImageTaskRecordService.java @@ -0,0 +1,80 @@ +package com.sonic.cow.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.cow.domain.entity.GenImageTask; +import com.sonic.cow.domain.entity.GenImageTaskRecord; +import com.sonic.cow.domain.output.GenImageListOutput; + +import java.util.List; + +/** + * 生成图片任务子项表 + * + * @author code + */ +public interface GenImageTaskRecordService extends IService { + + /** + * 保存明细数据 + * + * @param genImageTask + * @param taskIdList + */ + void saveRecord(GenImageTask genImageTask, List taskIdList); + + /** + * 根据图片生成任务的Id获取基础数据 + * + * @param runPodTaskId + * @return + */ + GenImageTaskRecord getByThirdTaskId(String runPodTaskId); + + /** + * 生成图片完成,更新数据库状态和url + * + * @param id + * @param imageUrl + * @param status + * @return + */ + void genImageCompleted(Long id, String imageUrl, String status, String prompt, String asyncTaskId, String failReason, Integer type); + + /** + * 根据任务ID查询已完成图片生成的总数 + * + * @param taskId + * @return + */ + Integer queryCompletedTaskCountByTaskId(Long taskId); + + /** + * 根据任务ID查询已完成图片生成的总数 + * + * @param batchNo + * @return + */ + Integer queryCompletedTaskCountByBatchNo(String batchNo); + + /** + * 根据批次号查询所有基础信息 + * + * @param batchNo + * @return + */ + List queryByBatchNo(String batchNo); + + /** + * 根据图片url获取对应 prompt + * + * @param userId + * @param imageUrl + * @return + */ + String getInitPromptByImageUrl(Long userId, String imageUrl); + + /** + * 添加相册图片,背景图片,ai形象图时,检查图片是否是AI生成的 + */ + void checkImageIsAIGenerated(Long userId, List imgUrlList); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/GenImageTaskService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/GenImageTaskService.java new file mode 100644 index 0000000..3fa3288 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/GenImageTaskService.java @@ -0,0 +1,54 @@ +package com.sonic.cow.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.common.auth.domains.Session; +import com.sonic.cow.domain.entity.GenImageTask; +import com.sonic.cow.domain.input.GenImageCallbackInput; +import com.sonic.cow.domain.input.GenImageInput; +import com.sonic.cow.domain.output.GenImageListOutput; +import com.sonic.cow.domain.output.GenImageOutput; + +import java.util.List; + +/** + * 生成图片任务 + * @author code + */ +public interface GenImageTaskService extends IService { + + /** + * 生成图片 + * @param session + * @param input + * @return + */ + GenImageOutput genImage(Session session, GenImageInput input); + + /** + * 查询图片生成结果 + * @param userId + * @param batchNo + * @return + */ + List getImage(Long userId, String batchNo); + + /** + * 图片生成成功的回调 + * @param input + */ + void genImageCallback(GenImageCallbackInput input); + + /** + * 删除图片生成任务 + * @param userId + * @param batchNo + */ + void delGenImageTask(Long userId, String batchNo); + + /** + * 扫描心跳过期的数据进行释放处理 + * @return + */ + Integer scanHeartBeatExpTime(); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/GenSupContentService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/GenSupContentService.java new file mode 100644 index 0000000..9d5223c --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/GenSupContentService.java @@ -0,0 +1,46 @@ +package com.sonic.cow.service; + +import com.sonic.cow.domain.input.GenSupContentInput; +import com.sonic.cow.domain.output.GenSupContentOutput; + +import java.util.List; + +public interface GenSupContentService { + + /** + * 生成辅助对话内容 + * + * @param userId + * @param input + * @return + */ + GenSupContentOutput genSupContent(Long userId, GenSupContentInput input); + + /** + * 用户超过3分钟停留没有发消息了生成自动聊天内容并发送IM消息 + * + * @param userId + * @param aiId + */ + void genAutoChatAndSendImMessage(Long userId, Long aiId); + + /** + * 生成辅助对话内容 + * + * @param userId + * @param aiId + * @param batchNo + * @return + */ + @Deprecated + List genSupContentV2(Long userId, Long aiId, String batchNo); + + /** + * 获取AI创建的辅助对话内容 + * + * @param userId + * @param aiId + * @return + */ + List aiCreateGenSupContent(Long userId, Long aiId); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/InputRequestBuildService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/InputRequestBuildService.java new file mode 100644 index 0000000..94a4c63 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/InputRequestBuildService.java @@ -0,0 +1,78 @@ +package com.sonic.cow.service; + +import com.sonic.cow.client.input.voicechat.domain.UserPrompt; +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.event.outer.payload.AiChatPayload; +import com.sonic.frog.lib.output.AiInfoApiOutput; +import com.sonic.pigeon.lib.output.HistoryMessageOutput; + +import java.util.List; + +/** + * 聊天入参构建 + */ +public interface InputRequestBuildService { + + /** + * 构造聊天入参 + * + * @param payload + * @param inputRequests + */ + void buildV2(AiChatPayload payload, List inputRequests, Integer historyMessageLimit, Boolean isText); + + /** + * 构造开启语音通话的开场白的聊天上下文 + * + * @param inputRequests + * @param systemPrompt + * @param historyMessageList + */ + void buildStartVoiceChatDialoguePrologue(Long fromUserId, Long toUserId, List inputRequests,String systemPrompt, List historyMessageList); + + /** + * 构造语音通话系统提示语 + * + * @param fromUserId + * @param toUserId + * @return + */ + String buildVoiceChatSystemPromptContent(Long fromUserId, Long toUserId, AiInfoApiOutput aiUserInfo); + + /** + * 构造系统提示语 + * + * @param fromUserId + * @param toUserId + * @return + */ + String buildSystemPromptContent(Long fromUserId, Long toUserId, AiInfoApiOutput aiUserInfo); + + + /** + * 获取用户与ai的历史消息 + * + * @param fromUserId + * @param toUserId + * @param limitNum + * @return + */ + List getHistoryMessageList(Long fromUserId, Long toUserId, Integer limitNum); + + /** + * 构建语音通话用户提示语 + * + * @param fromUserId + * @param toUserId + * @return + */ + List buildVoiceChatUserPrompts(Long fromUserId, Long toUserId, List historyMessageList); + + /** + * 构建语音通话情绪打分提示语 + * + * @param content + * @return + */ + String buildVoiceCallEmotionScorePromptContent(String content); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/PromptConfigService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/PromptConfigService.java new file mode 100644 index 0000000..1b2ae9c --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/PromptConfigService.java @@ -0,0 +1,15 @@ +package com.sonic.cow.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.cow.domain.entity.PromptConfig; + +public interface PromptConfigService extends IService { + + /** + * 根据提示词类型获取模板 + * @param promptType + * @return + */ + PromptConfig getPromptTemplate(String promptType); + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceChatRecordService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceChatRecordService.java new file mode 100644 index 0000000..7bdc5c6 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceChatRecordService.java @@ -0,0 +1,58 @@ +package com.sonic.cow.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.cow.domain.entity.VoiceChatRecord; +import com.sonic.cow.enums.VoiceChatStatusEnum; + +/** + * 语音聊天记录 + * + * @author code + */ +public interface VoiceChatRecordService extends IService { + + /** + * 新增聊天通话记录 + * + * @param userId + * @param aiId + * @param roomId + * @param taskId + */ + Long add(Long userId, Long aiId, String roomId, String taskId, String endpoint); + + /** + * 更新状态 + * + * @param roomId + * @param taskId + * @param status + */ + void updateStatus(String roomId, String taskId, VoiceChatStatusEnum status); + + /** + * 更新通话时长 + * + * @param roomId + * @param taskId + * @param duration + */ + void updateDuration(String roomId, String taskId, Long duration); + + /** + * 通过roomId和taskId,获取聊天通话记录 + * + * @param roomId + * @param taskId + * @return + */ + VoiceChatRecord getByRoomIdAndTaskId(String roomId, String taskId); + + /** + * 通过taskId,获取聊天通话记录 + * + * @param taskId + * @return + */ + VoiceChatRecord getByTaskId(String taskId); +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceChatService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceChatService.java new file mode 100644 index 0000000..45b3231 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceChatService.java @@ -0,0 +1,80 @@ +package com.sonic.cow.service; + +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.domain.input.GenerateRtcTokenInput; +import com.sonic.cow.domain.input.VoiceChatOptInput; +import com.sonic.cow.domain.output.GenerateRtcTokenOutput; +import com.sonic.cow.domain.output.VoiceChatOptOutput; +import org.apache.ibatis.annotations.Param; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * @description: 语音通话 + * @author: mzc + * @date: 2025-07-30 11:12 + **/ +public interface VoiceChatService { + + /** + * 生成RTC令牌 + * + * @param currentUserId + * @param input + * @return + */ + GenerateRtcTokenOutput generateRtcToken(Long currentUserId, GenerateRtcTokenInput input); + + /** + * 语音通话操作 + * + * @param userId + * @param input + * @return + */ + VoiceChatOptOutput voiceChatOpt(Long userId, VoiceChatOptInput input); + + /** + * webhook回调通知 + * + * @param request + * @param response + */ + void webhook(HttpServletRequest request, HttpServletResponse response); + + /** + * 会话状态回调通知(监听智能体状态) + * 文档: https://docs.byteplus.com/en/docs/byteplus-rtc/docs-1415216#receiving-via-server + * + * @param request + * @param response + */ + void conversationStateCallback(@Param("request") HttpServletRequest request, @Param("response") HttpServletResponse response); + + /** + * 实时字幕回调 + * + * @param request + * @param response + */ + void rtsCallback(HttpServletRequest request, HttpServletResponse response); + + /** + * 语音通话结束处理 + * + * @param userId + * @param aiId + * @param voiceChatInputRequestList 语音通话对话内容列表 + * @param duration + */ + void voiceCallEndHandler(Long userId, Long aiId, List voiceChatInputRequestList, Long duration); + + /** + * 停止语音通话(用户离开1分钟,杀app,刷新网页,关闭网页情况下) + * + * @param userId + */ + Boolean stopVoiceChat(Long userId); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceService.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceService.java new file mode 100644 index 0000000..490112b --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceService.java @@ -0,0 +1,56 @@ +package com.sonic.cow.service; + +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.domain.input.GenerateRtcTokenInput; +import com.sonic.cow.domain.input.VoiceAsrInput; +import com.sonic.cow.domain.input.VoiceChatOptInput; +import com.sonic.cow.domain.input.VoiceTtsV2Input; +import com.sonic.cow.domain.output.AsrOutput; +import com.sonic.cow.domain.output.GenerateRtcTokenOutput; +import com.sonic.cow.domain.output.VoiceChatOptOutput; +import org.apache.ibatis.annotations.Param; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @description: 语音通话 + * @author: mzc + * @date: 2025-07-30 11:12 + **/ +public interface VoiceService { + + /** + * 语音识别 + * + * @param userId 用户ID + * @param input base64编码音频内容 + * @return + */ + AsrOutput asr(Long userId, VoiceAsrInput input) throws UnirestException, InterruptedException; + + /** + * 语音生成 + * + * @param userId + * @param input + * @return + */ + String tts(Long userId, VoiceTtsV2Input input); + + /** + * 语音扣费 + * @param userId + * @param aiId + */ + void ttsDeAmount(Long userId, Long aiId); + + /** + * 检测余额是否不足 + * + * @param userId + * @param amount 本次金额 + * @return + */ + boolean checkBalanceIsInsufficient(Long userId, Long amount); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceV2Service.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceV2Service.java new file mode 100644 index 0000000..31fb7b6 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/VoiceV2Service.java @@ -0,0 +1,32 @@ +package com.sonic.cow.service; + +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.domain.output.AsrOutput; + +import java.io.File; + +/** + * @description: 语音通话 + * @author: mzc + * @date: 2025-07-30 11:12 + **/ +public interface VoiceV2Service { + + /** + * 语音识别 + * + * @param userId 用户ID + * @param file 文件 + * @return + */ + AsrOutput asr(Long userId, Long aiId, File file) throws UnirestException; + + /** + * 检测余额是否不足 + * + * @param userId + * @param amount 本次金额 + * @return + */ + boolean checkBalanceIsInsufficient(Long userId, Long amount); +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/AiGenImageServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/AiGenImageServiceImpl.java new file mode 100644 index 0000000..53c2115 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/AiGenImageServiceImpl.java @@ -0,0 +1,103 @@ +package com.sonic.cow.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.sonic.cow.domain.bo.TextGenImagePromptParse; +import com.sonic.cow.domain.bo.TextPromptParse; +import com.sonic.cow.domain.input.AiGenImageInput; +import com.sonic.cow.enums.PromptTypeEnum; +import com.sonic.cow.service.AiGenImageService; +import com.sonic.cow.service.CommonSendMqService; +import com.sonic.cow.service.GenAiUserContentService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +import static com.sonic.cow.utils.Constant.COMMON_IMAGE_PROMPT; + +@Slf4j +@Service +public class AiGenImageServiceImpl implements AiGenImageService { + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private GenAiUserContentService aiUserContentService; + + + @Override + public void aiGenImage(AiGenImageInput input, String batchNo, List taskIdList) { + log.info("AiGenImageServiceImpl input:{}", JSON.toJSONString(input)); + String imageStylePrompt = input.getImageStylePrompt(); + String content = input.getContent(); + //基图url + String imageUrl = input.getImageUrl(); + //基图提示词 + String initPrompt = input.getInitPrompt(); + + if (StringUtils.isNotBlank(imageUrl) && StringUtils.isNotBlank(initPrompt)) { + //调用AI 合并新老形象描述 + try { + Map paramMap = Maps.newHashMap(); + paramMap.put("initPrompt", initPrompt); + paramMap.put("content", content); + TextPromptParse textPromptParse = aiUserContentService.genContent(PromptTypeEnum.MERGE_NEW_OLD_IMAGE_DESC.name(), paramMap, TextPromptParse.class); + if (textPromptParse != null) { + content = textPromptParse.getContent(); + } + } catch (Exception e) { + log.error("AiGenImageServiceImpl imageGenText error", e); + } + } + //初始化6组prompt + List promptList = Lists.newArrayList(); + try { + //调用AI 获取6组不同的prompt 形象风格prompt+形象描述内容+通用图像提示词 组成新的prompt + String sex = input.getSex() == null ? "" : input.getSex(); + Integer age = input.getAge() == null ? 20 : input.getAge(); + //拼接年龄性别 + String sexAgeStr = sex + "," + age + ","; + Map genPromptMap = Maps.newHashMap(); + genPromptMap.put("content", content); + genPromptMap.put("imageStylePrompt", imageStylePrompt); + genPromptMap.put("commonImagePrompt", COMMON_IMAGE_PROMPT); + genPromptMap.put("sex", sex); + genPromptMap.put("age", age); + genPromptMap.put("introduction", input.getIntroduction() == null ? "" : input.getIntroduction()); + TextGenImagePromptParse textGenImagePromptParse = aiUserContentService.genContent(PromptTypeEnum.TEXT_TO_IMAGE_PROMPT.name(), genPromptMap, TextGenImagePromptParse.class); + //拼接年龄性别 + promptList.add(sexAgeStr + textGenImagePromptParse.getPrompt1()); + promptList.add(sexAgeStr + textGenImagePromptParse.getPrompt2()); + promptList.add(sexAgeStr + textGenImagePromptParse.getPrompt3()); + promptList.add(sexAgeStr + textGenImagePromptParse.getPrompt4()); + promptList.add(sexAgeStr + textGenImagePromptParse.getPrompt5()); + promptList.add(sexAgeStr + textGenImagePromptParse.getPrompt6()); + } catch (Exception e) { + log.error("AiGenImageServiceImpl textGenImagePromptParse error", e); + } + //创建6组任务,发送MQ处理(根据6组不同的prompt生成6张图片,上传到S3,并鉴黄) + log.info("AiGenImageServiceImpl promptList:{}", JSON.toJSONString(promptList)); + //没有,直接返回 + if (CollectionUtils.isEmpty(promptList)) { + return; + } + for (int i = 0; i < promptList.size(); i++) { + String prompt = promptList.get(i); + //任务id + String taskId = taskIdList.get(i); + //发送MQ处理 + if (StringUtils.isEmpty(imageUrl)) { + //文生图 + commonSendMqService.aiTextGenImageSendMq(batchNo, taskId, prompt, imageUrl); + } else { + //图生图 + commonSendMqService.aiImageGenImageSendMq(batchNo, taskId, prompt, imageUrl); + } + } + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/AiMessageServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/AiMessageServiceImpl.java new file mode 100644 index 0000000..e2590de --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/AiMessageServiceImpl.java @@ -0,0 +1,35 @@ +package com.sonic.cow.service.impl; + +import com.sonic.cow.domain.input.AiMessageDelInput; +import com.sonic.cow.service.AiMessageService; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.frog.lib.client.AiChatInfoClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class AiMessageServiceImpl implements AiMessageService { + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private AiChatInfoClient aiChatInfoClient; + + @Override + public void aiMessageDelete(AiMessageDelInput input, Long currentUserId) { + List aiIdList = input.getAiIdList(); + //删除 24小时未聊天时,AI自动给用户发送一条消息 + aiIdList.forEach(aiId -> { + String ftUserId = currentUserId + "-" + aiId; + stringRedisTemplate.opsForZSet().remove(redisKeyUtils.notifyUserZSetKey(), ftUserId); + }); + //更新删除消息了 + aiChatInfoClient.updateIsDelChatted(currentUserId, aiIdList); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ChatHandlerServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ChatHandlerServiceImpl.java new file mode 100644 index 0000000..8681400 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ChatHandlerServiceImpl.java @@ -0,0 +1,254 @@ +package com.sonic.cow.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.sonic.cow.client.ResponseChatClient; +import com.sonic.cow.client.request.ContentRequest; +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.client.request.Tools; +import com.sonic.cow.client.response.ResponseChatResponse; +import com.sonic.cow.domain.entity.PromptConfig; +import com.sonic.cow.enums.DeductionTypeEnum; +import com.sonic.cow.enums.PromptTypeEnum; +import com.sonic.cow.event.outer.payload.AiChatPayload; +import com.sonic.cow.event.outer.payload.CalcHeartbeatLevelPayload; +import com.sonic.cow.event.outer.payload.UserDeductionStatPayload; +import com.sonic.cow.service.*; +import com.sonic.cow.tools.GetAiImageArgs; +import com.sonic.cow.tools.ToolsDefinition; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; +import com.sonic.pigeon.lib.bo.AttachBo; +import com.sonic.pigeon.lib.bo.ScoreExtension; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.enums.MessageTypeEnum; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import com.sonic.pigeon.lib.input.SendAiTextMessageInput; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; + +@Slf4j +@Service +public class ChatHandlerServiceImpl implements ChatHandlerService { + + @Autowired + private ContextSessionCacheService contextSessionCacheService; + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private ResponseChatClient responseChatClient; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private InputRequestBuildService inputRequestBuildService; + @Autowired + private FunctionCallService functionCallService; + @Autowired + private PromptConfigService promptConfigService; + + @Override + public void handle(AiChatPayload payload, AttachBo attachBo) { + //获取上下文缓存 + String responseId = contextSessionCacheService.getByRedis(payload.getFromUserId(), payload.getToUserId()); + log.info("===> AiChatHandler content : {}, responseId : {}", payload.getContent(), responseId); + //组装出消息列表数据 + List inputRequests = Lists.newArrayList(); + //获取历史上下文,创建上下文缓存 + if (StringUtils.isEmpty(responseId)) { + inputRequestBuildService.buildV2(payload, inputRequests,100,true); + } + log.info("===> AiChatHandler responseId : {}", responseId); + long startTime = System.currentTimeMillis(); + if(MessageTypeEnum.CUSTOM == payload.getMessageType() && attachBo != null && "IMAGE".equalsIgnoreCase(attachBo.getType()) && StringUtils.isNotBlank(attachBo.getUrl())) { + //图片加尺寸 + inputRequests.add(InputRequest.builder() + .role(ChatMessageRole.USER) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(payload.getContent()).build(), + ContentRequest.builder().type("input_image").imageUrl(attachBo.getUrl() + "?x-oss-process=image/resize,w_200,h_200").build())) + .build()); + } else { + inputRequests.add(InputRequest.builder() + .role(ChatMessageRole.USER) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(payload.getContent()).build())) + .build()); + } + + //获取图片 + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.GET_AI_IMAGE_FUNCTION_CALL.name()); + List tools = Lists.newArrayList(ToolsDefinition.getAiImageToolsV2("get_ai_image", systemPromptConfig.getPromptTemplate())); + GetAiImageArgs getAiImageArgs = GetAiImageArgs.builder() + .userId(payload.getFromUserId()) + .aiId(payload.getToUserId()) + .build(); + //上下文聊天 + ResponseChatResponse chatResponse = responseChatClient.responseStructuredChat(responseId, null, inputRequests, tools, JSONObject.toJSONString(getAiImageArgs), null, null); + log.info("===> chatResponse : {}", chatResponse); + if(chatResponse == null) { + return; + } + //异常码判断 + if(chatResponse.getCode() != null) { + //根据异常码判断处理redis数据删除 + if("InvalidParameter.PreviousResponseNotFound".equalsIgnoreCase(chatResponse.getCode())) { + contextSessionCacheService.deleteRedis(payload.getFromUserId(), payload.getToUserId()); + return; + } + } + + log.info("===> AiChatHandler chat time : {} ms", System.currentTimeMillis() - startTime); + //处理方法回调 + if(chatResponse.getFunctionCallName() != null && chatResponse.getFunctionCallArguments() != null) { + //RPC 获取图片 并发送 + AIUserAlbumApiOutput album = functionCallService.callFunction(chatResponse); + //有回调ID的时候才进行回调 + if(StringUtils.isNotEmpty(chatResponse.getFunctionCallId())) { + //工具调用后反馈调用聊天 + chatResponse = functionCallFeedbackChat(responseId, inputRequests, chatResponse, album); + } else { + if(album == null) { + chatResponse.setMessage("Go check out my photo album"); + } else { + chatResponse.setMessage("This is my photo; you need to unlock it to view it"); + } + } + if(album == null) { + //发送文本IM消息 + sendImMessage(payload, chatResponse); + } else { + //发送自定义Im消息 + sendImCustomMessage(payload, chatResponse.getMessage(), album, chatResponse); + } + } else { + //发送IM消息 + sendImMessage(payload, chatResponse); + } + //是否存在清理缓存的任务数据 + boolean hasKey = stringRedisTemplate.hasKey(redisKeyUtils.chatResponseIdClearTaskKey(payload.getFromUserId(), payload.getToUserId())); + if(hasKey) { + //清理掉缓存数据 + contextSessionCacheService.deleteRedis(payload.getFromUserId(), payload.getToUserId()); + //删除掉任务数据 + stringRedisTemplate.delete(redisKeyUtils.chatResponseIdClearTaskKey(payload.getFromUserId(), payload.getToUserId())); + } else { + //将缓存id更新到缓存中去,过期时间3天 + if(StringUtils.isNotEmpty(chatResponse.getResponseId())) { + contextSessionCacheService.saveToRedis(payload.getFromUserId(), payload.getToUserId(), chatResponse.getResponseId()); + } + } + //记录到redis中,24小时后会主动给用户发送一条消息 + String ftUserId = payload.getFromUserId() + "-" + payload.getToUserId(); + stringRedisTemplate.opsForZSet().add(redisKeyUtils.notifyUserZSetKey(), ftUserId, System.currentTimeMillis() / 1000); + + //发送MQ计算心动等级 + commonSendMqService.calHeartbeatLevelSendMq(CalcHeartbeatLevelPayload.builder() + .userId(payload.getFromUserId()) + .aiId(payload.getToUserId()) + .heartbeatVal(chatResponse.getScore() == null ? new BigDecimal(0) : chatResponse.getScore()) + .type(CalcHeartbeatLevelPayload.Type.CHAT) + .build()); + //发送用户文本预扣费统计 + HashMap extraMap = Maps.newHashMap(); + extraMap.put("userContent", payload.getContent()); + extraMap.put("aiContent", chatResponse.getMessage()); + commonSendMqService.userDeductionStatSendMq(UserDeductionStatPayload.builder() + .userId(payload.getFromUserId()) + .aiId(payload.getToUserId()) + .deductionType(DeductionTypeEnum.TEXT) + .bizTime(LocalDateTime.now()) + .extra(JSON.toJSONString(extraMap)) + .build()); + } + + /** + * 函数调用反馈 + * @param responseId + * @param inputRequests + * @param chatResponse + * @param album + */ + private ResponseChatResponse functionCallFeedbackChat(String responseId, List inputRequests, ResponseChatResponse chatResponse, AIUserAlbumApiOutput album) { + //TODO 获取图片来决定回什么内容 + //组装出消息列表数据 + inputRequests.add(InputRequest.builder() + .callId(chatResponse.getFunctionCallId()) + .name(chatResponse.getFunctionCallName()) + .arguments(chatResponse.getFunctionCallArguments()) + .type("function_call") + .build()); + inputRequests.add(InputRequest.builder() + .callId(chatResponse.getFunctionCallId()) + .output(album != null ? "This is my photo; you need to unlock it to view it" : "Go check out my photo album") + .type("function_call_output") + .build()); + + //上下文聊天 + return responseChatClient.responseStructuredChat(responseId, null, inputRequests, null, null, null, null); + } + + /** + * 发送Im消息 + * @param payload + * @param chatResponse + */ + private void sendImMessage(AiChatPayload payload, ResponseChatResponse chatResponse) { + if(chatResponse == null || StringUtils.isEmpty(chatResponse.getMessage())) { + return; + } + // 发送消息给用户 + SendAiTextMessageInput sendAiTextMessageInput = new SendAiTextMessageInput(); + sendAiTextMessageInput.setFromUserId(payload.getToUserId()); + sendAiTextMessageInput.setToUserId(payload.getFromUserId()); + sendAiTextMessageInput.setContent(chatResponse.getMessage()); + //在扩展字段中 增加一个分值字段来进行处理 + sendAiTextMessageInput.setExtension(chatResponse.getScore() == null ? null : JSONObject.toJSONString(ScoreExtension.builder().score(chatResponse.getScore()).build())); + imMessageClient.sendAiToUserTextMessage(sendAiTextMessageInput); + } + + /** + * 发送Im消息 + * @param payload + * @param album + * @param chatResponse + */ + @Override + public void sendImCustomMessage(AiChatPayload payload, String message, AIUserAlbumApiOutput album, ResponseChatResponse chatResponse) { + if(chatResponse == null || StringUtils.isEmpty(chatResponse.getMessage())) { + return; + } + //图片地址不为空则发送图片的自定义IM消息 + SendAiCustomerMessageInput sendAiCustomerMessageInput = new SendAiCustomerMessageInput(); + sendAiCustomerMessageInput.setFromUserId(payload.getToUserId()); + sendAiCustomerMessageInput.setToUserId(payload.getFromUserId()); + sendAiCustomerMessageInput.setContent(message); + if(album != null) { + sendAiCustomerMessageInput.setAttachment(JSONObject.toJSONString(AttachBo.builder() + .type("IMAGE") + .height("2560") + .width("1440") + .unlockPrice(album.getUnlockPrice()) + .albumId(album.getAlbumId()) + .url(album.getImgUrl()) + .build()) + ); + } + //在扩展字段中 增加一个分值字段来进行处理 + sendAiCustomerMessageInput.setExtension(chatResponse.getScore() == null ? null : JSONObject.toJSONString(ScoreExtension.builder().score(chatResponse.getScore()).build())); + imMessageClient.sendAiToUserCustomerMessage(sendAiCustomerMessageInput); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ChatMemoryServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ChatMemoryServiceImpl.java new file mode 100644 index 0000000..ad46028 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ChatMemoryServiceImpl.java @@ -0,0 +1,53 @@ +package com.sonic.cow.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Lists; +import com.sonic.cow.service.ChatMemoryService; +import com.sonic.cow.utils.RedisKeyUtils; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.sonic.cow.utils.Constant.MAX_HISTORY_MESSAGE; +import static com.sonic.cow.utils.Constant.TTL_HISTORY_MESSAGE_DAY; + +@Slf4j +@Service +public class ChatMemoryServiceImpl implements ChatMemoryService { + + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private StringRedisTemplate stringRedisTemplate; + + + @Override + public List getHistory(Long userId, Long aiId) { + List historyList = stringRedisTemplate.opsForList().range(redisKeyUtils.chatMemoryCacheKey(userId, aiId), 0, MAX_HISTORY_MESSAGE); + //对象转换 + List messages = Lists.newArrayList(); + for (String historyMessage : historyList) { + ChatMessage message = JSONObject.parseObject(historyMessage, ChatMessage.class); + messages.add(message); + } + return messages; + } + + @Override + public void saveHistory(Long userId, Long aiId, List chatMessages) { + String redisKey = redisKeyUtils.chatMemoryCacheKey(userId, aiId); + for (ChatMessage message : chatMessages) { + stringRedisTemplate.opsForList().leftPush(redisKey, JSONObject.toJSONString(message)); + } + // 保留最近100条 + stringRedisTemplate.opsForList().trim(redisKey, 0, MAX_HISTORY_MESSAGE); + // 设置360天过期 + stringRedisTemplate.expire(redisKey, TTL_HISTORY_MESSAGE_DAY, TimeUnit.DAYS); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ChatServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ChatServiceImpl.java new file mode 100644 index 0000000..56e55fd --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ChatServiceImpl.java @@ -0,0 +1,64 @@ +package com.sonic.cow.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.google.common.collect.Lists; +import com.sonic.cow.client.ContextChatClient; +import com.sonic.cow.service.ChatMemoryService; +import com.sonic.cow.service.ChatService; +import com.sonic.cow.service.ContextSessionCacheService; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessage; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.sonic.cow.utils.Constant.TTL_CONTEXT_CACHING; + +@Slf4j +@Service +public class ChatServiceImpl implements ChatService { + + @Autowired + private ContextChatClient contextChatClient; + @Autowired + private ContextSessionCacheService contextSessionCacheService; + @Autowired + private ChatMemoryService chatMemoryService; + + @Override + public String chat(Long userId, Long aiId, String message) { + //STEP1:获取上下文id + String contextId = contextSessionCacheService.getContextId(userId, aiId); + if(StringUtils.isEmpty(contextId)) { + List messages = Lists.newArrayList(); + //TODO 获取系统提示词并设置 + messages.add(ChatMessage.builder() + .role(ChatMessageRole.SYSTEM) + .content("不允许回复火星文、表情符这些内容") + .build()); + //获取对话历史数据 + List historyList = chatMemoryService.getHistory(userId, aiId); + messages.addAll(historyList); + //创建上下文ID的缓存 + contextId = contextChatClient.createContextCaching(messages, TTL_CONTEXT_CACHING); + } + //STEP2:执行聊天的操作 + String result = contextChatClient.contextChat(contextId, message); + //STEP3:保存历史上下文 + List chatMessageList = Lists.newArrayList(); + chatMessageList.add(ChatMessage.builder() + .role(ChatMessageRole.USER) + .content(message) + .build()); + chatMessageList.add(ChatMessage.builder() + .role(ChatMessageRole.ASSISTANT) + .content(result) + .build()); + chatMemoryService.saveHistory(userId, aiId, chatMessageList); + //TODO 发送IM消息给用户 + log.debug("===> ASSISTANT result Message : {}", result); + return result; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/CommonSendMqServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/CommonSendMqServiceImpl.java new file mode 100644 index 0000000..ebe2ba7 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/CommonSendMqServiceImpl.java @@ -0,0 +1,168 @@ +package com.sonic.cow.service.impl; + + +import com.sonic.common.event.Event; +import com.sonic.common.event.EventProducer; +import com.sonic.common.event.RabbitmqEventProducer; +import com.sonic.cow.event.inner.payload.AiGenImagePayload; +import com.sonic.cow.event.inner.payload.EmotionScorePayload; +import com.sonic.cow.event.inner.payload.VoiceCallDeductionPayload; +import com.sonic.cow.event.inner.payload.VoiceCallWebhookPayload; +import com.sonic.cow.event.outer.payload.CalcHeartbeatLevelPayload; +import com.sonic.cow.event.outer.payload.UserBalanceInsufficientCheckoutPayload; +import com.sonic.cow.event.outer.payload.UserDeductionStatPayload; +import com.sonic.cow.service.CommonSendMqService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +import static com.sonic.cow.event.inner.EventType.*; +import static com.sonic.cow.event.outer.EventType.*; + +/** + * @description: 发送消息到im的mq + * @author: mzc + * @create: 2020-02-06 17:56 + **/ +@Service +@Slf4j +public class CommonSendMqServiceImpl implements CommonSendMqService { + + @Autowired + private EventProducer eventProducer; + + @Autowired + @Qualifier("aiTextGenImageMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta aiTextGenImageMeta; + @Autowired + @Qualifier("aiImageGenImageMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta aiImageGenImageMeta; + + @Autowired + @Qualifier("calcHeartbeatLevelMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta calcHeartbeatLevelMeta; + + @Autowired + @Qualifier("emotionScoreMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta emotionScoreMeta; + @Autowired + @Qualifier("userDeductionStatMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta userDeductionStatMeta; + + @Autowired + @Qualifier("voiceCallDeductionDeadMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta voiceCallDeductionDeadMeta; + @Autowired + @Qualifier("voiceCallDeductionMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta voiceCallDeductionMeta; + @Autowired + @Qualifier("voiceCallWebhookMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta voiceCallWebhookMeta; + + @Autowired + @Qualifier("userBalanceInsufficientCheckoutMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta userBalanceInsufficientCheckoutMeta; + + + @Override + public void aiTextGenImageSendMq(String batchNo, String taskId, String prompt, String imageUrl) { + eventProducer.send(Event.builder() + .eventScene(AI_TEXT_GEN_IMAGE.getEventCode().getScene()) + .eventModule(AI_TEXT_GEN_IMAGE.getEventCode().getModule()) + .eventName(AI_TEXT_GEN_IMAGE.getEventCode().getName()) + .data(AiGenImagePayload.builder().batchNo(batchNo).taskId(taskId).prompt(prompt).imageUrl(imageUrl).build()).build(), aiTextGenImageMeta); + } + + @Override + public void aiImageGenImageSendMq(String batchNo, String taskId, String prompt, String imageUrl) { + eventProducer.send(Event.builder() + .eventScene(AI_IMAGE_GEN_IMAGE.getEventCode().getScene()) + .eventModule(AI_IMAGE_GEN_IMAGE.getEventCode().getModule()) + .eventName(AI_IMAGE_GEN_IMAGE.getEventCode().getName()) + .data(AiGenImagePayload.builder().batchNo(batchNo).taskId(taskId).prompt(prompt).imageUrl(imageUrl).build()).build(), aiImageGenImageMeta); + } + + @Override + public void calHeartbeatLevelSendMq(CalcHeartbeatLevelPayload payload) { + eventProducer.send(Event.builder() + .eventScene(CALC_HEARTBEAT_LEVEL.getEventCode().getScene()) + .eventModule(CALC_HEARTBEAT_LEVEL.getEventCode().getModule()) + .eventName(CALC_HEARTBEAT_LEVEL.getEventCode().getName()) + .data(payload).build(), calcHeartbeatLevelMeta); + } + + @Override + public void emotionScoreSendMq(EmotionScorePayload payload) { + eventProducer.send(Event.builder() + .eventScene(EMOTION_SCORE.getEventCode().getScene()) + .eventModule(EMOTION_SCORE.getEventCode().getModule()) + .eventName(EMOTION_SCORE.getEventCode().getName()) + .data(payload).build(), emotionScoreMeta); + } + + @Override + public void userDeductionStatSendMq(UserDeductionStatPayload payload) { + eventProducer.send(Event.builder() + .eventScene(USER_DEDUCTION_STAT.getEventCode().getScene()) + .eventModule(USER_DEDUCTION_STAT.getEventCode().getModule()) + .eventName(USER_DEDUCTION_STAT.getEventCode().getName()) + .data(payload).build(), userDeductionStatMeta); + } + + @Override + public void voiceCallDeductionSendDeadMq(Long userId, Long aiId, Long voiceCallId) { + VoiceCallDeductionPayload payload = VoiceCallDeductionPayload.builder() + .userId(userId) + .aiId(aiId) + .voiceCallId(voiceCallId) + .build(); + eventProducer.send(new Event( + VOICE_CALL_DEDUCTION.getEventCode().getScene(), + VOICE_CALL_DEDUCTION.getEventCode().getModule(), + VOICE_CALL_DEDUCTION.getEventCode().getName(), + message -> { + //1分钟 这里单位是毫秒 + message.getMessageProperties().setExpiration("60000"); + return message; + }, + payload), voiceCallDeductionDeadMeta); + } + + @Override + public void voiceCallDeductionSendMq(Long userId, Long aiId, Long voiceCallId) { + VoiceCallDeductionPayload payload = VoiceCallDeductionPayload.builder() + .userId(userId) + .aiId(aiId) + .voiceCallId(voiceCallId) + .build(); + eventProducer.send(Event.builder() + .eventScene(VOICE_CALL_DEDUCTION.getEventCode().getScene()) + .eventModule(VOICE_CALL_DEDUCTION.getEventCode().getModule()) + .eventName(VOICE_CALL_DEDUCTION.getEventCode().getName()) + .data(payload).build(), voiceCallDeductionMeta); + } + + @Override + public void voiceCallWebhookSendMq(com.sonic.cow.client.rtc.Event event) { + VoiceCallWebhookPayload payload = VoiceCallWebhookPayload.builder() + .voiceCallEvent(event) + .build(); + eventProducer.send(Event.builder() + .eventScene(VOICE_CALL_WEBHOOK.getEventCode().getScene()) + .eventModule(VOICE_CALL_WEBHOOK.getEventCode().getModule()) + .eventName(VOICE_CALL_WEBHOOK.getEventCode().getName()) + .data(payload).build(), voiceCallWebhookMeta); + } + + @Override + public void userBalanceInsufficientCheckoutMq(Long userId) { + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(USER_BALANCE_INSUFFICIENT_CHECKOUT.getEventCode().getScene()) + .eventModule(USER_BALANCE_INSUFFICIENT_CHECKOUT.getEventCode().getModule()) + .eventName(USER_BALANCE_INSUFFICIENT_CHECKOUT.getEventCode().getName()) + .data(UserBalanceInsufficientCheckoutPayload.builder() + .userId(userId).build()).build(), userBalanceInsufficientCheckoutMeta); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ContextSessionCacheServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ContextSessionCacheServiceImpl.java new file mode 100644 index 0000000..54da109 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/ContextSessionCacheServiceImpl.java @@ -0,0 +1,51 @@ +package com.sonic.cow.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.cow.dao.ContextSessionCacheDao; +import com.sonic.cow.domain.entity.ContextSessionCache; +import com.sonic.cow.service.ContextSessionCacheService; +import com.sonic.cow.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class ContextSessionCacheServiceImpl extends ServiceImpl implements ContextSessionCacheService { + + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public String getContextId(Long userId, Long toUserId) { + return stringRedisTemplate.opsForValue().get(redisKeyUtils.chatContextIdCacheKey(userId, toUserId)); + } + + @Override + public void saveToRedis(Long fromUserId, Long toUserId, String responseId) { + //写入redis缓存 3天 + String redisKey = redisKeyUtils.chatResponseIdCacheKey(fromUserId, toUserId); + log.info("===> saveToRedis redisKey : {}, responseId : {}", redisKey, responseId); + stringRedisTemplate.opsForValue().set(redisKey, responseId, 3, TimeUnit.DAYS); + } + + @Override + public String getByRedis(Long fromUserId, Long toUserId) { + String redisKey = redisKeyUtils.chatResponseIdCacheKey(fromUserId, toUserId); + String responseId = stringRedisTemplate.opsForValue().get(redisKey); + log.info("===> getByRedis redisKey : {}, responseId : {}", redisKey, responseId); + return responseId; + } + + @Override + public void deleteRedis(Long fromUserId, Long toUserId) { + String redisKey = redisKeyUtils.chatResponseIdCacheKey(fromUserId, toUserId); + stringRedisTemplate.delete(redisKey); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/FunctionCallServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/FunctionCallServiceImpl.java new file mode 100644 index 0000000..77ac264 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/FunctionCallServiceImpl.java @@ -0,0 +1,45 @@ +package com.sonic.cow.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sonic.cow.client.response.ResponseChatResponse; +import com.sonic.cow.service.FunctionCallService; +import com.sonic.cow.tools.GetAiImageArgs; +import com.sonic.frog.lib.client.AiUserAlbumClient; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class FunctionCallServiceImpl implements FunctionCallService { + + @Autowired + private AiUserAlbumClient aiUserAlbumClient; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public AIUserAlbumApiOutput callFunction(ResponseChatResponse response) { + //处理获取AI未解锁图片的方法调用 + if ("get_ai_image".equals(response.getFunctionCallName())) { + //解析入参 + String argumentsJson = response.getFunctionCallArguments(); + log.info("===> callFunction argumentsJson : {}", argumentsJson); + GetAiImageArgs tool_args = null; + try { + tool_args = objectMapper.readValue(argumentsJson, GetAiImageArgs.class); + } catch (JsonProcessingException e) { + log.error("===> callFunction 解析 get_ai_image 参数时出错: " + argumentsJson + " - " + e.getMessage()); + } + //调用方法获取待解锁图片 + AIUserAlbumApiOutput toolResult = aiUserAlbumClient.getRandomLockImage(tool_args.getUserId(), tool_args.getAiId()); + log.info("===> callFunction 工具执行结果 id : {}, result : {}", response.getFunctionCallId(), toolResult); + //返回 图片 + return toolResult; + } + return null; + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenAiUserContentServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenAiUserContentServiceImpl.java new file mode 100644 index 0000000..8ec5c78 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenAiUserContentServiceImpl.java @@ -0,0 +1,139 @@ +package com.sonic.cow.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.sonic.cow.client.ContextChatClient; +import com.sonic.cow.domain.bo.AiGen4Bo; +import com.sonic.cow.domain.bo.ExtJsonBo; +import com.sonic.cow.domain.entity.PromptConfig; +import com.sonic.cow.domain.input.GenAiUserContentV1Input; +import com.sonic.cow.domain.output.GenAiUserContentV1Output; +import com.sonic.cow.enums.PromptTypeEnum; +import com.sonic.cow.enums.SexEnums; +import com.sonic.cow.enums.ToastResultCode; +import com.sonic.cow.service.GenAiUserContentService; +import com.sonic.cow.service.PromptConfigService; +import com.sonic.cow.utils.DateUtils; +import com.sonic.cow.utils.LimitUtils; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.cow.utils.TextCleaner; +import freemarker.template.Configuration; +import freemarker.template.Template; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.StringWriter; +import java.util.Map; + +/** + * 一键生成AI人物基础信息 + */ +@Slf4j +@Service +public class GenAiUserContentServiceImpl implements GenAiUserContentService { + + @Autowired + private PromptConfigService promptConfigService; + @Autowired + private ContextChatClient contextChatClient; + @Autowired + private LimitUtils limitUtils; + @Autowired + private RedisKeyUtils redisKeyUtils; + + + private static final Configuration configuration = new Configuration(Configuration.VERSION_2_3_29); + + @Override + public GenAiUserContentV1Output genAiUserContentV1(Long userId, GenAiUserContentV1Input input) throws Exception { + //处理入参中的性别的问题 + if (StringUtils.isNotEmpty(input.getSex())) { + input.setSex(SexEnums.getSex(input.getSex())); + } + //处理生日为 age + if (StringUtils.isNotEmpty(input.getBirthday())) { + //将生日转换成年龄 + input.setAge(DateUtils.calculateAge(input.getBirthday())); + } + //处理介绍,默认为空字符串,不能要报错 + if (input.getIntroduction() == null) { + input.setIntroduction(""); + } + if(userId != -1) { + //创建,编辑语音合成,1分钟内20次的限制 + boolean b = limitUtils.defaultLimitCheckByKey(redisKeyUtils.contentLimitKey(userId, input.getPtType()), 20, 60); + ToastResultCode.REQUEST_LIMIT_ERROR.check(b); + } + + //从数据库中获取系统提示词 + PromptConfig promptConfig = promptConfigService.getPromptTemplate(input.getPtType().name()); + if (promptConfig == null) { + return null; + } + //获取对应的【人格特征】系统提示词(基于 性格|标签 的方式进行拼接的类型配置) + PromptConfig personalityTraitsPromptConfig = promptConfigService.getPromptTemplate(input.getCharacterCode() + "|" + input.getTagCode()); + //系统提示词替换(根据参数名替换) + Map data = JSONObject.parseObject(JSONObject.toJSONString(input), Map.class); + //将人格的提示词内容,填充到入参对象中 + if (personalityTraitsPromptConfig != null) { + data.put("personalityTraits", personalityTraitsPromptConfig.getPromptTemplate()); + } + String formatted = processTemplate(promptConfig.getPromptTemplate(), data); + AiGen4Bo bo = contextChatClient.aiGen4(formatted, promptConfig.getModel(), promptConfig.getTemperature(), promptConfig.getTopp()); + log.info("===> genAiUserContentV1 input : {}, temperature : {}, result : {}", formatted, promptConfig.getTemperature(), bo.getContent()); + //处理返回结果标签 + String result = TextCleaner.removeAiCharacterPrompt(bo.getContent()); + GenAiUserContentV1Output output = new GenAiUserContentV1Output(); + if(PromptTypeEnum.EXTRACT_JSON_CONTENT == input.getPtType()) { + try { + ExtJsonBo extJsonBo = JSONObject.parseObject(result, ExtJsonBo.class); + output.setContent(JSONObject.toJSONString(extJsonBo)); + } catch (Exception e) { + log.error("===> genAiUserContentV1 json parse error ", e); + //解析失败的话,直接构造一个空对象给前端 + ExtJsonBo extJsonBo = new ExtJsonBo(); + output.setContent(JSONObject.toJSONString(extJsonBo)); + } + } else { + output.setContent(result); + } + output.setInput(bo.getInput()); + output.setOutput(bo.getOutput()); + output.setUrl(bo.getUrl()); + return output; + } + + /** + * 处理模板参数内容的替换 + * + * @param templateString + * @param data + * @return + * @throws Exception + */ + public static String processTemplate(String templateString, Map data) throws Exception { + Template template = new Template("template", templateString, configuration); + StringWriter writer = new StringWriter(); + template.process(data, writer); + return writer.toString(); + } + + + @Override + public T genContent(String promptType, Object input, Class clazz) throws Exception { + //从数据库中获取系统提示词 + PromptConfig promptConfig = promptConfigService.getPromptTemplate(promptType); + if (promptConfig == null) { + return null; + } + //系统提示词替换(根据参数名替换) + Map data = JSONObject.parseObject(JSONObject.toJSONString(input), Map.class); + String formatted = processTemplate(promptConfig.getPromptTemplate(), data); + //调用模型进行创作,并格式化输出 + String result = contextChatClient.aiGen3(formatted, promptConfig.getModel(), promptConfig.getTemperature(), promptConfig.getTopp()); + log.info("===> genContent result : {}", result); + result = result.replaceAll("```json", "").replaceAll("```", ""); + return JSONObject.parseObject(result, clazz); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenImageTaskRecordServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenImageTaskRecordServiceImpl.java new file mode 100644 index 0000000..9665032 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenImageTaskRecordServiceImpl.java @@ -0,0 +1,128 @@ +package com.sonic.cow.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.sonic.cow.dao.GenImageTaskRecordDao; +import com.sonic.cow.domain.entity.GenImageTask; +import com.sonic.cow.domain.entity.GenImageTaskRecord; +import com.sonic.cow.domain.output.GenImageListOutput; +import com.sonic.cow.enums.ToastResultCode; +import com.sonic.cow.service.GenImageTaskRecordService; +import com.sonic.cow.utils.BeanConvert; +import com.sonic.cow.utils.MD5Util; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 生成图片任务子项表 + * + * @author code + */ +@Slf4j +@Service +public class GenImageTaskRecordServiceImpl extends ServiceImpl implements GenImageTaskRecordService { + + @Override + public void saveRecord(GenImageTask genImageTask, List taskIdList) { + List list = Lists.newArrayList(); + for (String taskId : taskIdList) { + GenImageTaskRecord genImageTaskRecord = new GenImageTaskRecord(); + genImageTaskRecord.setTaskId(genImageTask.getId()); + genImageTaskRecord.setBatchNo(genImageTask.getBatchNo()); + genImageTaskRecord.setUserId(genImageTask.getUserId()); + genImageTaskRecord.setThirdTaskId(taskId); + genImageTaskRecord.setPrompt(null); + //先给一个默认值1,好走索引 + genImageTaskRecord.setImageUrlMd5("1"); + genImageTaskRecord.setStatus(GenImageTaskRecord.Status.PENDING); + genImageTaskRecord.setCompleted(false); + genImageTaskRecord.setCreateTime(LocalDateTime.now()); + genImageTaskRecord.setEditTime(LocalDateTime.now()); + list.add(genImageTaskRecord); + } + if (CollectionUtils.isNotEmpty(list)) { + saveBatch(list); + } + } + + @Override + public GenImageTaskRecord getByThirdTaskId(String thirdTaskId) { + GenImageTaskRecord genImageTaskRecord = getOne(Wrappers.lambdaQuery() + .eq(GenImageTaskRecord::getThirdTaskId, thirdTaskId)); + return genImageTaskRecord; + } + + @Override + public void genImageCompleted(Long id, String imageUrl, String status, String prompt, String asyncTaskId, String failReason, Integer type) { + String imageUrlMd5 = null; + if (StringUtils.isNotEmpty(imageUrl)) { + imageUrlMd5 = MD5Util.digest(imageUrl); + } + update(Wrappers.lambdaUpdate() + .set(GenImageTaskRecord::getImageUrl, imageUrl) + .set(GenImageTaskRecord::getImageUrlMd5, imageUrlMd5) + .set(GenImageTaskRecord::getCompleted, "PENDING".equals(status) ? false : true) + .set(GenImageTaskRecord::getStatus, status) + .set(GenImageTaskRecord::getFailReason, failReason) + .set(GenImageTaskRecord::getPrompt, prompt) + .set(GenImageTaskRecord::getAsyncTaskId, asyncTaskId) + .set(GenImageTaskRecord::getType, type) + .eq(GenImageTaskRecord::getId, id)); + } + + @Override + public Integer queryCompletedTaskCountByTaskId(Long taskId) { + int count = count(Wrappers.lambdaQuery() + .eq(GenImageTaskRecord::getTaskId, taskId) + .eq(GenImageTaskRecord::getCompleted, true)); + return count; + } + + @Override + public Integer queryCompletedTaskCountByBatchNo(String batchNo) { + int count = count(Wrappers.lambdaQuery() + .eq(GenImageTaskRecord::getBatchNo, batchNo) + .eq(GenImageTaskRecord::getCompleted, true)); + return count; + } + + + @Override + public List queryByBatchNo(String batchNo) { + List list = list(Wrappers.lambdaQuery() + .select(GenImageTaskRecord::getImageUrl, GenImageTaskRecord::getStatus, GenImageTaskRecord::getPrompt) + .eq(GenImageTaskRecord::getBatchNo, batchNo)); + List outputList = BeanConvert.copeList(list, GenImageListOutput.class); + return outputList; + } + + @Override + public String getInitPromptByImageUrl(Long userId, String imageUrl) { + if (StringUtils.isEmpty(imageUrl)) { + return null; + } + //将url转换成MD5的字段进行查询 + String imageUrlMd5 = MD5Util.digest(imageUrl); + GenImageTaskRecord genImageTaskRecord = getOne(Wrappers.lambdaQuery() + .eq(GenImageTaskRecord::getUserId, userId) + .eq(GenImageTaskRecord::getImageUrlMd5, imageUrlMd5) + .last(" limit 1") + ); + return genImageTaskRecord != null ? genImageTaskRecord.getPrompt() : null; + } + + @Override + public void checkImageIsAIGenerated(Long userId, List imgUrlList) { + ToastResultCode.IMAGE_NOT_ILLEGAL.check(CollectionUtils.isEmpty(imgUrlList)); + List imgUrlMd5List = imgUrlList.stream().map(imgUrl -> MD5Util.digest(imgUrl)).collect(Collectors.toList()); + int count = count(Wrappers.lambdaQuery().eq(GenImageTaskRecord::getUserId, userId).in(GenImageTaskRecord::getImageUrlMd5, imgUrlMd5List)); + ToastResultCode.IMAGE_NOT_ILLEGAL.check(imgUrlList.size() != count); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenImageTaskServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenImageTaskServiceImpl.java new file mode 100644 index 0000000..314426d --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenImageTaskServiceImpl.java @@ -0,0 +1,311 @@ +package com.sonic.cow.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.utils.LogUtils; +import com.sonic.cow.client.ContextChatClient; +import com.sonic.cow.dao.GenImageTaskDao; +import com.sonic.cow.domain.bo.AiUserCacheInfo; +import com.sonic.cow.domain.bo.LimitBo; +import com.sonic.cow.domain.entity.GenImageTask; +import com.sonic.cow.domain.entity.GenImageTaskRecord; +import com.sonic.cow.domain.input.AiGenImageInput; +import com.sonic.cow.domain.input.GenImageCallbackInput; +import com.sonic.cow.domain.input.GenImageInput; +import com.sonic.cow.domain.output.AiGenImageOutput; +import com.sonic.cow.domain.output.GenImageListOutput; +import com.sonic.cow.domain.output.GenImageOutput; +import com.sonic.cow.enums.BizResultCode; +import com.sonic.cow.enums.CreateImageType; +import com.sonic.cow.enums.SexEnums; +import com.sonic.cow.enums.ToastResultCode; +import com.sonic.cow.service.AiGenImageService; +import com.sonic.cow.service.GenImageTaskRecordService; +import com.sonic.cow.service.GenImageTaskService; +import com.sonic.cow.utils.*; +import com.sonic.frog.lib.client.AiChatInfoClient; +import com.sonic.frog.lib.client.AiClient; +import com.sonic.frog.lib.client.AiUserAlbumClient; +import com.sonic.frog.lib.enums.HeartbeatLevelEnum; +import com.sonic.frog.lib.output.AiChatInfoOutput; +import com.sonic.frog.lib.output.AiInfoApiOutput; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.enums.BizType; +import com.sonic.lion.lib.input.BalanceCheckoutInput; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * 生成图片任务 + * + * @author code + */ +@Slf4j +@Service +public class GenImageTaskServiceImpl extends ServiceImpl implements GenImageTaskService { + + @Autowired + private GenImageTaskDao genImageTaskDao; + @Autowired + private GenImageTaskRecordService genImageTaskRecordService; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private LimitUtils limitUtils; + @Autowired + private CacheUtils cacheUtils; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private AiGenImageService aiGenImageService; + @Autowired + private AiClient aiClient; + @Autowired + private AiUserAlbumClient aiUserAlbumClient; + @Autowired + private PayClient payClient; + @Autowired + private AiChatInfoClient aiChatInfoClient; + @Autowired + private ContextChatClient contextChatClient; + + private static final Integer GEN_IMAGE_MAX_SIZE = 10; + + private static final Integer EXP_TIME = 24 * 60 * 60; + + /** + * 异步任务处理的线程池 + */ + private ExecutorService executor = new ThreadPoolExecutor(10, // 核心线程数(假设CPU核心数为8,IO密集型任务) + 30, // 最大线程数 + 60, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(500), // 限制队列大小为500 + new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:让提交者线程执行 + ); + + + @Transactional(rollbackFor = Exception.class) + @Override + public GenImageOutput genImage(Session session, GenImageInput input) { + //处理生日为 age + if (StringUtils.isNotEmpty(input.getBirthday())) { + //将生日转换成年龄 + input.setAge(DateUtils.calculateAge(input.getBirthday())); + } + //类型 + CreateImageType createImageType = input.getType(); + + Long currentUserId = session.getUserId(); + String message = ToastResultCode.GEN_IMAGE_LIMIT_ERROR.getErrorMsg(); + //TODO 限流的逻辑:用户限流、IP限流、设备限流 +// LimitBo userLimitBo = limitUtils.defaultLimitCheckByKeyV2(redisKeyUtils.genImageUserLimit(currentUserId, input.getHl()), GEN_IMAGE_MAX_SIZE, EXP_TIME); +// ToastResultCode.GEN_IMAGE_LIMIT_ERROR.check(userLimitBo.getLimitBl(), CheckUtils.getErrorMessage(userLimitBo, message)); +// LimitBo deviceLimitBo = limitUtils.defaultLimitCheckByKeyV2(redisKeyUtils.genImageDeviceLimit(input.getDeviceId(), input.getHl()), GEN_IMAGE_MAX_SIZE, EXP_TIME); +// ToastResultCode.GEN_IMAGE_LIMIT_ERROR.check(deviceLimitBo.getLimitBl(), CheckUtils.getErrorMessage(deviceLimitBo, message)); +// LimitBo ipLimitBo = limitUtils.defaultLimitCheckByKeyV2(redisKeyUtils.genImageIpLimit(input.getIp(), input.getHl()), GEN_IMAGE_MAX_SIZE, EXP_TIME); +// ToastResultCode.GEN_IMAGE_LIMIT_ERROR.check(ipLimitBo.getLimitBl(), CheckUtils.getErrorMessage(ipLimitBo, message)); + //个人介绍 敏感词检测 +// contextChatClient.nsfwCheck(input.getIntroduction()); + //生成批次号 + String batchNo = CheckUtils.genUUIDAnEncode(); + //生成6个任务id + List taskIdList = Lists.newArrayList(); + int taskCount = 6; + for (int i = 0; i < taskCount; i++) { + taskIdList.add(UUID.randomUUID().toString()); + } + //解析提示词创建生成图片的异步任务,得到任务id列表 + AiGenImageInput aiGenImageInput = new AiGenImageInput(); + aiGenImageInput.setImageStylePrompt(input.getImageStylePrompt()); + aiGenImageInput.setContent(input.getContent()); + if (input.getHl() != null && input.getHl()) { + //图片主人id + Long imageUserId = currentUserId; + //权限判断,查询该aiId是否是当前用户的 + AiInfoApiOutput aiInfo = aiClient.getAiInfo(input.getAiId()); + if (CreateImageType.CREATE_AI_IMAGE.equals(createImageType)) { + //创建AI形象,1分钟内20次的限制 + boolean b = limitUtils.defaultLimitCheckByKey(redisKeyUtils.aiImageLimitKey(currentUserId), 20, 60); + ToastResultCode.REQUEST_LIMIT_ERROR.check(b); + } + //编辑形象,相像图片时才做权限判断 + if (CreateImageType.EDIT_AI_IMAGE.equals(createImageType) || CreateImageType.ALBUM.equals(createImageType)) { + //是否本人操作 + ToastResultCode.SYS_PERMISSION_DENIED.check(aiInfo != null && !aiInfo.getUserId().equals(currentUserId)); + //创建次数检验,创作次数扣减 + aiUserAlbumClient.useCreateCount(currentUserId); + } + //创作自定义背景,需要支付20Coin + if (CreateImageType.BACKGROUND.equals(createImageType)) { + //检测心动等级有没达到10级,10级才能创建背景 + AiChatInfoOutput aiChatInfo = aiChatInfoClient.getAiChatInfo(currentUserId, input.getAiId()); + List unlockHearbeatLevelList = aiChatInfo.getUnlockHearbeatLevelList(); + ToastResultCode.SYS_PERMISSION_DENIED.check(!unlockHearbeatLevelList.contains(HeartbeatLevelEnum.LEVEL_10)); + //发起支付 + createBackgroundPay(currentUserId); + //创建背景时需要使用AI用户的形象图片主人id + imageUserId = aiInfo.getUserId(); + } + //查询出AI用户的形象图片 + aiGenImageInput.setImageUrl(aiInfo != null ? aiInfo.getBaseImageUrl() : null); + //基图的initPrompt + aiGenImageInput.setInitPrompt(genImageTaskRecordService.getInitPromptByImageUrl(imageUserId, aiGenImageInput.getImageUrl())); + } else { + aiGenImageInput.setImageReferenceUrl(input.getImageReferenceUrl()); + } + //处理入参中的性别的问题 + if (StringUtils.isNotEmpty(input.getSex())) { + aiGenImageInput.setSex(SexEnums.getSex(input.getSex())); + } + aiGenImageInput.setAge(input.getAge()); + aiGenImageInput.setIntroduction(input.getIntroduction()); + aiGenImageInput.setBirthday(input.getBirthday()); + + String traceId = LogUtils.getTraceId(); + //线程池异步执行 + executor.execute(() -> { + try { + LogUtils.setTraceId(traceId); + //调用Ai生成图片 + aiGenImageService.aiGenImage(aiGenImageInput, batchNo, taskIdList); + } catch (Exception e) { + log.error("aiGenImage error:", e); + } + }); + + //保存数据库的逻辑 + GenImageTask genImageTask = new GenImageTask(); + genImageTask.setUserId(currentUserId); + genImageTask.setBatchNo(batchNo); + genImageTask.setTaskCount(taskCount); + genImageTask.setCompletedCount(0); + genImageTask.setPollingCount(0); + genImageTask.setStatus(GenImageTask.Status.PENDING); + genImageTask.setEndpoint(input.getEndpoint()); + genImageTask.setDeviceId(input.getDeviceId()); + genImageTask.setIpAddress(input.getIp()); + genImageTask.setCreateTime(LocalDateTime.now()); + genImageTask.setEditTime(LocalDateTime.now()); + save(genImageTask); + + //保存明细数据 + genImageTaskRecordService.saveRecord(genImageTask, taskIdList); + + //组装出参数据 + GenImageOutput output = new GenImageOutput(); + output.setBatchNo(batchNo); + return output; + } + + /** + * 自定义背景付费 + * + * @param currentUserId + */ + private void createBackgroundPay(Long currentUserId) { + //需要调用支付服务扣钱20Coin + String orderNo = KeyGenerator.instance().generatorUniqueKey(""); + BalanceCheckoutInput balanceCheckoutInput = BalanceCheckoutInput.builder() + .platform("Balance") + .outTradeNo(orderNo) + .bizType(BizType.CREATE_AI_IMAGE) + .name(BizType.CREATE_AI_IMAGE.getDesc()) + //付款人 + .srcAccountId(currentUserId) + //收款人 + .desAccountId(-1L) + //总金额 + .productAmount(7000L) + //折扣金额 + .promoAmount(0L) + .build(); + payClient.checkoutToB(balanceCheckoutInput); + } + + @Override + public List getImage(Long userId, String batchNo) { + //图片已完成数 + int completedCount = genImageTaskRecordService.queryCompletedTaskCountByBatchNo(batchNo); + //图片未处理完成时来获取结果,做时间判断和限流控制 + if (completedCount == 0) { + //解析出批次号中的时间字段,超过5分钟的批次号直接快速返回掉不做任何处理 + Long createTime = CheckUtils.uuidDecodeToTime(batchNo); + if (createTime + 300 < System.currentTimeMillis() / 1000) { + BizResultCode.GEN_IMAGE_TIMEOUT.check(true); + } + //redis接口请求限流 每5s内只能请求一次 + boolean limitBl = limitUtils.defaultLimitCheckByKey(redisKeyUtils.genImagePollingLimit(batchNo), 1, 5); + if (limitBl) { + return Lists.newArrayList(); + } + } + //从缓存中获取数据,回调时清理缓存 + //查询数据库获取图片生成结果 + List outputList = cacheUtils.getCacheListAndSet(redisKeyUtils.getGenImageCacheKey(batchNo), GenImageListOutput.class, () -> { + List queryOutputList = genImageTaskRecordService.queryByBatchNo(batchNo); + //如果鉴黄了,不返回地址 + for (GenImageListOutput genImageListOutput : queryOutputList) { + if (GenImageTaskRecord.Status.NSFW.equals(genImageListOutput.getStatus())) { + genImageListOutput.setImageUrl(null); + } + } + return JSONObject.toJSONString(queryOutputList); + }, 60); + //更新任务表的心跳时间字段,需要根据心跳时间来关闭掉任务 + genImageTaskDao.updateHeartBeat(batchNo); + return outputList; + } + + @Override + public void genImageCallback(GenImageCallbackInput input) { + GenImageTaskRecord genImageTaskRecord = genImageTaskRecordService.getByThirdTaskId(input.getThirdTaskId()); + if (genImageTaskRecord == null) { + return; + } + //更新数据库状态 + genImageTaskRecordService.genImageCompleted(genImageTaskRecord.getId(), input.getImageUrl(), input.getStatus(), input.getPrompt(), input.getAsyncTaskId(), input.getFailReason(), input.getType()); + //查询已生成任务完成的数据总数 + int completedCount = genImageTaskRecordService.queryCompletedTaskCountByTaskId(genImageTaskRecord.getTaskId()); + //统计生成数量,判断数据库的已完成条数已经和当前已完成条数计算出状态并进行更新 + genImageTaskDao.updateCompletedCount(genImageTaskRecord.getTaskId(), completedCount); + //清理缓存数据 + stringRedisTemplate.delete(redisKeyUtils.getGenImageCacheKey(genImageTaskRecord.getBatchNo())); + } + + @Override + public void delGenImageTask(Long userId, String batchNo) { + //更新数据库状态为已停止,防止被定时任务扫描到进行处理 + update(Wrappers.lambdaUpdate() + .set(GenImageTask::getStatus, GenImageTask.Status.RELEASED) + .eq(GenImageTask::getBatchNo, batchNo) + .eq(GenImageTask::getStatus, GenImageTask.Status.PENDING)); + } + + @Override + public Integer scanHeartBeatExpTime() { + //考虑网络延迟的问题,这里先设置成18秒内的心跳次数3次 + LocalDateTime startTime = LocalDateTime.now().minusSeconds(18); + List taskList = genImageTaskDao.scanHeartBeatExpTime(startTime); + for (GenImageTask genImageTask : taskList) { + delGenImageTask(null, genImageTask.getBatchNo()); + } + return taskList.size(); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenSupContentServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenSupContentServiceImpl.java new file mode 100644 index 0000000..4468c9e --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/GenSupContentServiceImpl.java @@ -0,0 +1,282 @@ +package com.sonic.cow.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import com.google.common.collect.Lists; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.sonic.bear.lib.client.UserNicknamePoolClient; +import com.sonic.cow.client.ResponseChatClient; +import com.sonic.cow.client.request.ContentRequest; +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.client.response.ResponseChatResponse; +import com.sonic.cow.domain.entity.PromptConfig; +import com.sonic.cow.domain.input.GenSupContentInput; +import com.sonic.cow.domain.output.GenSupContentOutput; +import com.sonic.cow.enums.PromptTypeEnum; +import com.sonic.cow.event.outer.payload.AiChatPayload; +import com.sonic.cow.service.ContextSessionCacheService; +import com.sonic.cow.service.GenSupContentService; +import com.sonic.cow.service.InputRequestBuildService; +import com.sonic.cow.service.PromptConfigService; +import com.sonic.cow.utils.KeyGenerator; +import com.sonic.cow.utils.LimitUtils; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.enums.BizType; +import com.sonic.lion.lib.input.BalanceCheckoutInput; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.input.SendAiTextMessageInput; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.HashMap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Slf4j +@Service +public class GenSupContentServiceImpl implements GenSupContentService { + + + @Autowired + private ResponseChatClient responseChatClient; + @Autowired + private ContextSessionCacheService contextSessionCacheService; + @Autowired + private InputRequestBuildService inputRequestBuildService; + @Autowired + private PromptConfigService promptConfigService; + @Autowired + private PayClient payClient; + @Autowired + private LimitUtils limitUtils; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private UserNicknamePoolClient userNicknamePoolClient; + + @Override + public GenSupContentOutput genSupContent(Long userId, GenSupContentInput input) { + GenSupContentOutput output = new GenSupContentOutput(); + boolean checkout = true; + if(!StringUtils.isEmpty(input.getBatchNo())) { + Boolean hasKey = stringRedisTemplate.hasKey(redisKeyUtils.genSupContentBatchNoLimitKey(userId, input.getBatchNo())); + if(hasKey) { + //从redis中获取是否有限流次数,30s内有效 + checkout = limitUtils.defaultLimitCheckByKey(redisKeyUtils.genSupContentBatchNoLimitKey(userId, input.getBatchNo()), 3, 30); + } + } + if(checkout) { + //调用支付服务,扣款 + checkout(userId); + //生成一个批次号,并添加到缓存中 + String batchNo = KeyGenerator.instance().generatorUniqueKey(""); + limitUtils.defaultLimitCheckByKey(redisKeyUtils.genSupContentBatchNoLimitKey(userId, batchNo), 3, 30); + output.setBatchNo(batchNo); + } + + //获取上下文缓存 + String responseId = contextSessionCacheService.getByRedis(userId, input.getAiId()); + List inputRequests = Lists.newArrayList(); + if (responseId == null) { + AiChatPayload payload = AiChatPayload.builder().fromUserId(userId).toUserId(input.getAiId()).build(); + //调用公共方法,组装入参数据 + inputRequestBuildService.buildV2(payload, inputRequests,30,true); + } + //数据库查询模板数据 + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.GEN_SUP_CONTENT_SYSTEM_PROMPT_TEMPLATE.name()); + String systemPrompt = CollectionUtils.isEmpty(input.getExcContentList()) ? systemPromptConfig.getPromptTemplate() : String.format(systemPromptConfig.getPromptTemplate1(), input.getExcContentList()); + + //批量查询昵称 + Map nicknameMap = userNicknamePoolClient.batchGetNickname(Lists.newArrayList(userId, input.getAiId())); + log.debug("===> nicknameMap : {}", nicknameMap); + Map param = new HashMap<>(2); + param.put("userNickname", nicknameMap.get(userId)); + param.put("aiNickname", nicknameMap.get(input.getAiId())); + //处理系统提示词 + String systemPromptConfigContent = InputRequestBuildServiceImpl.processTemplate(systemPrompt, param); + + //构造系统提示词 + inputRequests.add(InputRequest.builder() + .role(ChatMessageRole.SYSTEM) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text") + .text(systemPromptConfigContent) + .build())).build()); + log.info("===> genSupContent inputRequests : {}", JSONObject.toJSONString(inputRequests)); + //执行聊天 不添加缓存 + ResponseChatResponse chatResponse = responseChatClient.responseChat(responseId, null, inputRequests, null, null, false); + if (chatResponse == null || StringUtils.isEmpty(chatResponse.getMessage())) { + return null; + } + output.setContentList(buildResult(chatResponse.getMessage())); + return output; + } + + @Override + public void genAutoChatAndSendImMessage(Long userId, Long aiId) { + //限流控制?24小时对单个AI只能触发3次 + boolean bl = limitUtils.defaultLimitCheckByKey(redisKeyUtils.genAutoChatAndSendImMessageLimitKey(userId, aiId), 3, 24 * 60 * 60); + if(bl) { + return; + } + //获取上下文缓存 + String responseId = contextSessionCacheService.getByRedis(userId, aiId); + List inputRequests = Lists.newArrayList(); + if (responseId == null) { + AiChatPayload payload = AiChatPayload.builder().fromUserId(userId).toUserId(aiId).build(); + //调用公共方法,组装入参数据 + inputRequestBuildService.buildV2(payload, inputRequests,30,true); + } + //数据库查询模板数据 + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.AUTO_SEND_CHAT_3_MINUTES_SYSTEM_PROMPT_TEMPLATE.name()); + + //批量查询昵称 + Map nicknameMap = userNicknamePoolClient.batchGetNickname(Lists.newArrayList(userId, aiId)); + log.debug("===> nicknameMap : {}", nicknameMap); + Map param = new HashMap<>(2); + param.put("userNickname", nicknameMap.get(userId)); + param.put("aiNickname", nicknameMap.get(aiId)); + //处理系统提示词 + String systemPromptConfigContent = InputRequestBuildServiceImpl.processTemplate(systemPromptConfig.getPromptTemplate(), param); + + //构造系统提示词 + inputRequests.add(InputRequest.builder() + .role(ChatMessageRole.SYSTEM) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text") + .text(systemPromptConfigContent) + .build())).build()); + log.info("===> genAutoChatAndSendImMessage inputRequests : {}", JSONObject.toJSONString(inputRequests)); + //执行聊天 不添加缓存 + ResponseChatResponse chatResponse = responseChatClient.responseStructuredChat(responseId, null, inputRequests, null, null, null, null); + log.info("===> genAutoChatAndSendImMessage chatResponse : {}", JSONObject.toJSONString(chatResponse)); + if (chatResponse == null || StringUtils.isEmpty(chatResponse.getMessage())) { + return; + } + + //将缓存id更新到缓存中去,过期时间3天 + if(StringUtils.isNotEmpty(chatResponse.getResponseId())) { + contextSessionCacheService.saveToRedis(userId, aiId, chatResponse.getResponseId()); + } + + // 发送消息给用户 + SendAiTextMessageInput sendAiTextMessageInput = new SendAiTextMessageInput(); + sendAiTextMessageInput.setFromUserId(aiId); + sendAiTextMessageInput.setToUserId(userId); + sendAiTextMessageInput.setContent(chatResponse.getMessage()); + imMessageClient.sendAiToUserTextMessage(sendAiTextMessageInput); + } + + /** + * 构建结果 + * @param text + * @return + */ + private List buildResult(String text) { + // 使用正则表达式匹配方括号内的 JSON 数组内容 + Pattern pattern = Pattern.compile("\\[.*?\\]", Pattern.DOTALL); + Matcher matcher = pattern.matcher(text); + if (matcher.find()) { + String jsonArrayStr = matcher.group(0); + // 使用 Gson 解析为 JsonArray + JsonParser parser = new JsonParser(); + JsonArray jsonArray = parser.parse(jsonArrayStr).getAsJsonArray(); + List result = Lists.newArrayList(); + // 遍历数组元素 + for (JsonElement element : jsonArray) { + result.add(element.getAsString()); + } + return result; + } + return Lists.newArrayList(); + } + + /** + * 调用支付服务扣款 + * + * @param userId + */ + private void checkout(Long userId) { + String orderNo = KeyGenerator.instance().generatorUniqueKey(""); + BalanceCheckoutInput balanceCheckoutInput = BalanceCheckoutInput.builder() + .platform("Balance") + .outTradeNo(orderNo) + .bizType(BizType.CHAT_ASSISTANT) + .name(BizType.CHAT_ASSISTANT.getDesc()) + //付款人 + .srcAccountId(userId) + //收款人 + .desAccountId(-1L) + //总金额 + .productAmount(100L) + //折扣金额 + .promoAmount(0L) + .build(); + payClient.checkoutToB(balanceCheckoutInput); + } + + @Override + public List genSupContentV2(Long userId, Long aiId, String batchNo) { + //调用支付服务,扣款 + checkout(userId); + //获取上下文缓存 + String responseId = contextSessionCacheService.getByRedis(userId, aiId); + List inputRequests = Lists.newArrayList(); + if (responseId == null) { + AiChatPayload payload = AiChatPayload.builder().fromUserId(userId).toUserId(aiId).build(); + //调用公共方法,组装入参数据 + inputRequestBuildService.buildV2(payload, inputRequests,30,true); + } + //数据库查询模板数据 + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.GEN_SUP_CONTENT_SYSTEM_PROMPT_TEMPLATE.name()); + //构造系统提示词 + inputRequests.add(InputRequest.builder() + .role(ChatMessageRole.SYSTEM) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(systemPromptConfig.getPromptTemplate()).build())).build()); + log.info("===> genSupContentV2 inputRequests : {}", JSONObject.toJSONString(inputRequests)); + //执行聊天 不添加缓存 + ResponseChatResponse chatResponse = responseChatClient.responseChat(responseId, null, inputRequests, null, null, false); + if (chatResponse == null || StringUtils.isEmpty(chatResponse.getMessage())) { + return null; + } + return JSONObject.parseArray(chatResponse.getMessage(), String.class); + } + + @Override + public List aiCreateGenSupContent(Long userId, Long aiId) { + //获取上下文缓存 + String responseId = contextSessionCacheService.getByRedis(userId, aiId); + List inputRequests = Lists.newArrayList(); + if (responseId == null) { + AiChatPayload payload = AiChatPayload.builder().fromUserId(userId).toUserId(aiId).build(); + //调用公共方法,组装入参数据 + inputRequestBuildService.buildV2(payload, inputRequests,30,true); + } + //数据库查询模板数据 + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.GEN_SUP_CONTENT_SYSTEM_PROMPT_TEMPLATE.name()); + String systemPrompt = systemPromptConfig.getPromptTemplate(); + //构造系统提示词 + inputRequests.add(InputRequest.builder() + .role(ChatMessageRole.SYSTEM) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(systemPrompt).build())).build()); + log.info("===> genSupContent inputRequests : {}", JSONObject.toJSONString(inputRequests)); + //执行聊天 不添加缓存 + ResponseChatResponse chatResponse = responseChatClient.responseChat(responseId, null, inputRequests, null, null, false); + if (chatResponse == null || StringUtils.isEmpty(chatResponse.getMessage())) { + return null; + } + return buildResult(chatResponse.getMessage()); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/InputRequestBuildServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/InputRequestBuildServiceImpl.java new file mode 100644 index 0000000..5d88f1e --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/InputRequestBuildServiceImpl.java @@ -0,0 +1,367 @@ +package com.sonic.cow.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.sonic.bear.lib.client.UserSearchClient; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.cow.client.input.voicechat.domain.UserPrompt; +import com.sonic.cow.client.request.ContentRequest; +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.domain.bo.AiChatPromptConfigBo; +import com.sonic.cow.domain.bo.ExtJsonBo; +import com.sonic.cow.domain.bo.ImMessageVoiceCallAttachBo; +import com.sonic.cow.domain.entity.PromptConfig; +import com.sonic.cow.enums.PromptTypeEnum; +import com.sonic.cow.enums.SexEnums; +import com.sonic.cow.event.outer.payload.AiChatPayload; +import com.sonic.cow.service.InputRequestBuildService; +import com.sonic.cow.service.PromptConfigService; +import com.sonic.cow.utils.BeanConvert; +import com.sonic.cow.utils.DateConvert; +import com.sonic.cow.utils.TimerUtils; +import com.sonic.frog.lib.client.AiChatInfoClient; +import com.sonic.frog.lib.client.AiClient; +import com.sonic.frog.lib.output.AiChatInfoOutput; +import com.sonic.frog.lib.output.AiInfoApiOutput; +import com.sonic.pigeon.lib.bo.AttachBo; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.enums.ImMessageTypeEnum; +import com.sonic.pigeon.lib.enums.MessageTypeEnum; +import com.sonic.pigeon.lib.input.HistoryMessageInput; +import com.sonic.pigeon.lib.output.HistoryMessageOutput; +import freemarker.template.Configuration; +import freemarker.template.Template; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.StringWriter; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class InputRequestBuildServiceImpl implements InputRequestBuildService { + + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private AiClient aiClient; + @Autowired + private UserSearchClient userSearchClient; + @Autowired + private PromptConfigService promptConfigService; + @Autowired + private AiChatInfoClient aiChatInfoClient; + + private static final Configuration configuration = new Configuration(Configuration.VERSION_2_3_29); + + @Override + public void buildV2(AiChatPayload payload, List inputRequests, Integer historyMessageLimit, Boolean isText) { + if (inputRequests == null) { + return; + } + HistoryMessageInput input = new HistoryMessageInput(); + input.setFromUserId(payload.getFromUserId()); + input.setToUserId(payload.getToUserId()); + input.setLimit(historyMessageLimit); + input.setDescending(false); + //rpc获取im的历史消息 + List historyMessageOutputs = imMessageClient.getHistoryMessage(input); + log.info("buildV2 historyMessageOutputs:{}", JSON.toJSONString(historyMessageOutputs)); + //构建inputRequestList + buildInputRequestList(payload.getFromUserId(), inputRequests, historyMessageOutputs); + + //获取AI基础信息 + AiInfoApiOutput aiUserInfo = aiClient.getAiInfo(payload.getToUserId()); + //构造系统提示词,并放到第一个位置 + InputRequest systemMessage; + if (isText) { + systemMessage = InputRequest.builder().role(ChatMessageRole.SYSTEM).content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(buildSystemPromptContent(payload.getFromUserId(), payload.getToUserId(), aiUserInfo)).build())).build(); + } else { + systemMessage = InputRequest.builder().role(ChatMessageRole.SYSTEM).content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(buildVoiceChatSystemPromptContent(payload.getFromUserId(), payload.getToUserId(), aiUserInfo)).build())).build(); + } + inputRequests.add(0, systemMessage); + //文本聊天 且 当历史消息条数不足的时候才构造开场白,也就是说小于20条的时候才构造,其他条件下不进行构造 + if (inputRequests.size() < 20) { + //构造开场白 + InputRequest dialoguePrologueMessage = InputRequest.builder().role(ChatMessageRole.ASSISTANT).content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(aiUserInfo.getDialoguePrologue()).build())).build(); + inputRequests.add(1, dialoguePrologueMessage); + } + } + + @Override + public void buildStartVoiceChatDialoguePrologue(Long fromUserId, Long toUserId, List inputRequests, String systemPrompt, List historyMessageList) { + buildInputRequestList(fromUserId, inputRequests, historyMessageList); + } + + /** + * 构建输入请求列表 + * + * @param fromUserId + * @param inputRequests + * @param historyMessageList + */ + private static void buildInputRequestList(Long fromUserId, List inputRequests, List historyMessageList) { + for (HistoryMessageOutput historyMessageOutput : historyMessageList) { + //自定义消息,除了送礼物,图片消息, 语音通话内容,其他的直接break; + if (MessageTypeEnum.CUSTOM == historyMessageOutput.getMessageType()) { + AttachBo attachBo = historyMessageOutput.getAttachment() == null ? null : JSONObject.parseObject(historyMessageOutput.getAttachment().toString(), AttachBo.class); + log.info("buildV2 attachBo:{}", attachBo); + if (attachBo != null && !ImMessageTypeEnum.IMAGE.name().equalsIgnoreCase(attachBo.getType()) + && !ImMessageTypeEnum.IM_SEND_GIFT.name().equalsIgnoreCase(attachBo.getType()) && !ImMessageTypeEnum.CALL.name().equalsIgnoreCase(attachBo.getType())) { + continue; + } + //语音通话内容,特殊处理,把语音通话内容组装成inputRequest + if (attachBo != null && ImMessageTypeEnum.CALL.name().equalsIgnoreCase(attachBo.getType())) { + ImMessageVoiceCallAttachBo imMessageVoiceCallAttachBo = JSONObject.parseObject(historyMessageOutput.getAttachment().toString(), ImMessageVoiceCallAttachBo.class); + log.info("buildV2 imMessageVoiceCallAttachBo:{}", attachBo); + if (imMessageVoiceCallAttachBo != null && StringUtils.isNotBlank(imMessageVoiceCallAttachBo.getVoiceChatInputRequestList())) { + List inputRequestList = JSONObject.parseArray(imMessageVoiceCallAttachBo.getVoiceChatInputRequestList(), InputRequest.class); + inputRequests.addAll(inputRequestList); + } + continue; + } + } + //发送人是用户,且偶数条数时组装 + if (fromUserId.equals(historyMessageOutput.getFromUserId()) && inputRequests.size() % 2 == 0) { + //组装用户消息 + List contentRequestList = Lists.newArrayList(); + contentRequestList.add(ContentRequest.builder().type("input_text").text(historyMessageOutput.getContent()).build()); + InputRequest message = InputRequest.builder().role(ChatMessageRole.USER).content(contentRequestList).build(); + inputRequests.add(message); + } else if (!fromUserId.equals(historyMessageOutput.getFromUserId()) && inputRequests.size() % 2 == 1) { + //奇数条数时组装 + //组装AI消息 + InputRequest message = InputRequest.builder().role(ChatMessageRole.ASSISTANT).content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(historyMessageOutput.getContent()).build())).build(); + inputRequests.add(message); + } + } + //组装出来的数据必须是偶数条,否则需要移除最后一条数据 + if (inputRequests.size() % 2 != 0) { + inputRequests.remove(inputRequests.size() - 1); + } + } + + + /** + * 获取用户与ai的历史消息 + * + * @param fromUserId + * @param toUserId + * @return + */ + public List getHistoryMessageList(Long fromUserId, Long toUserId, Integer limitNum) { + List historyMessageOutputs = Lists.newArrayList(); + try { + HistoryMessageInput input = new HistoryMessageInput(); + input.setFromUserId(fromUserId); + input.setToUserId(toUserId); + input.setLimit(limitNum); + input.setDescending(false); + //rpc获取im的历史消息 + return imMessageClient.getHistoryMessage(input); + } catch (Exception e) { + log.error("getHistoryMessage error:{}", e.getMessage()); + } + return historyMessageOutputs; + } + + @Override + public List buildVoiceChatUserPrompts(Long fromUserId, Long toUserId, List historyMessageList) { + List userPromptList = Lists.newArrayList(); + for (HistoryMessageOutput historyMessageOutput : historyMessageList) { + //自定义消息,除了送礼物,图片消息, 语音通话内容,其他的直接break; + if (MessageTypeEnum.CUSTOM == historyMessageOutput.getMessageType()) { + AttachBo attachBo = historyMessageOutput.getAttachment() == null ? null : JSONObject.parseObject(historyMessageOutput.getAttachment().toString(), AttachBo.class); + log.info("buildV2 attachBo:{}", attachBo); + if (attachBo != null && !ImMessageTypeEnum.IMAGE.name().equalsIgnoreCase(attachBo.getType()) + && !ImMessageTypeEnum.IM_SEND_GIFT.name().equalsIgnoreCase(attachBo.getType()) && !ImMessageTypeEnum.CALL.name().equalsIgnoreCase(attachBo.getType())) { + continue; + } + //语音通话内容,特殊处理,把语音通话内容组装成inputRequest + if (attachBo != null && ImMessageTypeEnum.CALL.name().equalsIgnoreCase(attachBo.getType())) { + ImMessageVoiceCallAttachBo imMessageVoiceCallAttachBo = JSONObject.parseObject(historyMessageOutput.getAttachment().toString(), ImMessageVoiceCallAttachBo.class); + log.info("buildV2 imMessageVoiceCallAttachBo:{}", attachBo); + if (imMessageVoiceCallAttachBo != null && StringUtils.isNotBlank(imMessageVoiceCallAttachBo.getVoiceChatInputRequestList())) { + List inputRequestList = JSONObject.parseArray(imMessageVoiceCallAttachBo.getVoiceChatInputRequestList(), InputRequest.class); + for (InputRequest inputRequest : inputRequestList) { + ChatMessageRole role = inputRequest.getRole(); + List contentRequestList = inputRequest.getContent(); + if (CollectionUtils.isNotEmpty(contentRequestList)) { + String content = contentRequestList.get(0).getText() != null ? contentRequestList.get(0).getText() : ""; + UserPrompt message = UserPrompt.builder().Role(role.value()).Content(content.replaceAll("(.*?)", "")).build(); + userPromptList.add(message); + } + } + } + continue; + } + } + String role = "assistant"; + //发送人是用户,且偶数条数时组装 + if (fromUserId.equals(historyMessageOutput.getFromUserId())) { + role = "user"; + } + if (StringUtils.isNotBlank(historyMessageOutput.getContent())) { + UserPrompt message = UserPrompt.builder().Role(role).Content(historyMessageOutput.getContent().replaceAll("(.*?)", "")).build(); + userPromptList.add(message); + } + } + return userPromptList; + } + + + /** + * 构造系统提示词 + * + * @param fromUserId + * @param toUserId + * @return + */ + @Override + public String buildSystemPromptContent(Long fromUserId, Long toUserId, AiInfoApiOutput aiUserInfo) { + //数据库查询模板数据 + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.CHAT_SYSTEM_PROMPT_TEMPLATE.name()); + return buildSystemPromptContent(fromUserId, toUserId, aiUserInfo, systemPromptConfig); + } + + /** + * 构造语音通话系统提示词 + * + * @param fromUserId + * @param toUserId + * @return + */ + public String buildVoiceChatSystemPromptContent(Long fromUserId, Long toUserId, AiInfoApiOutput aiUserInfo) { + //数据库查询模板数据 + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.VOICE_CHAT_SYSTEM_PROMPT_TEMPLATE.name()); + return buildSystemPromptContent(fromUserId, toUserId, aiUserInfo, systemPromptConfig); + } + + + /** + * 构造系统提示词 + * + * @param fromUserId + * @param toUserId + * @return + */ + public String buildSystemPromptContent(Long fromUserId, Long toUserId, AiInfoApiOutput aiUserInfo, PromptConfig systemPromptConfig) { + AiChatPromptConfigBo promptConfigBo = new AiChatPromptConfigBo(); + if(StringUtils.isNotEmpty(aiUserInfo.getUserProfileExtJson())) { + ExtJsonBo extJsonBo = JSONObject.parseObject(aiUserInfo.getUserProfileExtJson(), ExtJsonBo.class); + promptConfigBo = BeanConvert.copeBean(extJsonBo, AiChatPromptConfigBo.class); + } + //TODO 构造系统提示词 步骤1 获取ai基础信息 + long startTime = TimerUtils.start(); + promptConfigBo.setAiNickname(aiUserInfo.getNickname()); + promptConfigBo.setAiSex(SexEnums.getSex(aiUserInfo.getSex().toString())); + promptConfigBo.setAiBirthday(DateConvert.localDateTime2String(aiUserInfo.getBirthday())); + TimerUtils.logCost(startTime, "构造系统提示词 步骤1 获取ai基础信息"); + + //TODO 构造系统提示词 步骤2 查询AI个性字典获取提示词 + startTime = TimerUtils.start(); + //查询AI个性字典获取提示词 + PromptConfig personalityTraitsPromptConfig = promptConfigService.getPromptTemplate(aiUserInfo.getCharacterCode() + "|" + aiUserInfo.getTagCode()); + promptConfigBo.setAiPersonalityTraits(personalityTraitsPromptConfig.getPromptTemplate()); + promptConfigBo.setAiProfile(aiUserInfo.getProfile()); + promptConfigBo.setAiDialogueStyle(StringUtils.isNotEmpty(aiUserInfo.getDialogueStyle()) ? aiUserInfo.getDialogueStyle() : ""); + TimerUtils.logCost(startTime, "构造系统提示词 步骤2 查询AI个性字典获取提示词"); + + //TODO 构造系统提示词 步骤3 获取当前用户和AI的聊天基础设置 + startTime = TimerUtils.start(); + //获取当前用户和AI的聊天基础设置 + AiChatInfoOutput aiChatInfoOutput = aiChatInfoClient.getAiChatInfo(fromUserId, toUserId); + TimerUtils.logCost(startTime, "构造系统提示词 步骤3 获取当前用户和AI的聊天基础设置"); + + //TODO 构造系统提示词 步骤4 模板对象赋值 + startTime = TimerUtils.start(); + //处理认识天数+1处理 + aiChatInfoOutput.setDayCount(aiChatInfoOutput.getDayCount() == null ? 1 : aiChatInfoOutput.getDayCount() + 1); + if (StringUtils.isEmpty(aiChatInfoOutput.getNickname()) && aiChatInfoOutput.getSex() == null && aiChatInfoOutput.getBirthday() == null) { + //获取当前用户的基础信息 + BaseUserInfoOutput fromUserInfo = userSearchClient.baseUserInfo(fromUserId); + promptConfigBo.setUserNickname(fromUserInfo.getNickname()); + promptConfigBo.setUserSex(SexEnums.getSex(fromUserInfo.getSex().toString())); + promptConfigBo.setUserBirthday(DateConvert.localDateTime2String(fromUserInfo.getBirthday())); + } else { + promptConfigBo.setUserNickname(aiChatInfoOutput.getNickname()); + promptConfigBo.setUserSex(SexEnums.getSex(aiChatInfoOutput.getSex().toString())); + promptConfigBo.setUserBirthday(DateConvert.localDateTime2String(aiChatInfoOutput.getBirthday())); + } + //为空的话就是未填写 + promptConfigBo.setUserProfile(StringUtils.isEmpty(aiChatInfoOutput.getWhoAmI()) ? "" : aiChatInfoOutput.getWhoAmI()); + //查询提示词 + PromptConfig relationshipPromptConfig = promptConfigService.getPromptTemplate(aiChatInfoOutput.getRelationStage()); + //用户关系阶段的描述词 + promptConfigBo.setRelationshipDesc(relationshipPromptConfig.getPromptTemplate()); + //用户关系阶段 + promptConfigBo.setRelationship(relationshipPromptConfig.getPromptName()); + //用户关系阶段中禁用的提示词 + promptConfigBo.setRelationshipBanned(relationshipPromptConfig.getPromptTemplate1()); + //用户已认识天数 + promptConfigBo.setDaysKnown(aiChatInfoOutput.getDayCount()); + //设置用户ID和aiId【用String接收处理科学计数法的问题】 + promptConfigBo.setUserId(fromUserId.toString()); + promptConfigBo.setAiId(toUserId.toString()); + promptConfigBo.setDialoguePrologue(aiUserInfo.getDialoguePrologue()); + + //相识天数判断是否查询对话场景提示词 + if (aiChatInfoOutput.getDayCount() > 3) { + //查询提示词 + PromptConfig dialogueScenarioPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.DIALOGUE_SCENARIO.name()); + //根据meet关系来判断对话场景 + promptConfigBo.setDialogueScenario(aiChatInfoOutput.getMeet() != null && aiChatInfoOutput.getMeet() ? dialogueScenarioPromptConfig.getPromptTemplate() : ""); + } else { + promptConfigBo.setDialogueScenario(""); + } + + Map data = JSONObject.parseObject(JSONObject.toJSONString(promptConfigBo), Map.class); + String template = StringUtils.isNotEmpty(promptConfigBo.getUserProfile()) ? systemPromptConfig.getPromptTemplate() : systemPromptConfig.getPromptTemplate1(); + TimerUtils.logCost(startTime, "构造系统提示词 步骤4 模板对象赋值"); + + //执行真正的模板内容替换操作 + //TODO 构造系统提示词 步骤5 执行真正的模板内容替换操作 + startTime = TimerUtils.start(); + String content = processTemplate(template, data); + TimerUtils.logCost(startTime, "构造系统提示词 步骤5 执行真正的模板内容替换操作"); + return content; + } + + /** + * 处理模板参数内容的替换 + * + * @param templateString + * @param data + * @return + * @throws Exception + */ + public static String processTemplate(String templateString, Map data) { + try { + log.info("===> processTemplate data : {}, templateString : {}", data, templateString); + Template template = new Template("template", templateString, configuration); + StringWriter writer = new StringWriter(); + template.process(data, writer); + return writer.toString(); + } catch (Exception e) { + log.error("处理模板参数内容异常", e); + throw new RuntimeException(e); + } + } + + @Override + public String buildVoiceCallEmotionScorePromptContent(String content) { + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.CHAT_SCORE_SYSTEM_PROMPT_TEMPLATE.name()); + Map data = Maps.newHashMap(); + data.put("content", content); + String template = systemPromptConfig.getPromptTemplate(); + //执行真正的模板内容替换操作 + return processTemplate(template, data); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/PromptConfigServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/PromptConfigServiceImpl.java new file mode 100644 index 0000000..bbc9fbb --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/PromptConfigServiceImpl.java @@ -0,0 +1,24 @@ +package com.sonic.cow.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.cow.dao.PromptConfigDao; +import com.sonic.cow.domain.entity.PromptConfig; +import com.sonic.cow.service.PromptConfigService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class PromptConfigServiceImpl extends ServiceImpl implements PromptConfigService { + + + @Override + public PromptConfig getPromptTemplate(String promptType) { + return getOne(Wrappers.lambdaQuery().eq(PromptConfig::getPromptType, promptType) + .eq(PromptConfig::getIsDelete, false) + .orderByDesc(PromptConfig::getId) + .last("limit 1")); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceChatRecordServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceChatRecordServiceImpl.java new file mode 100644 index 0000000..6ed4abe --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceChatRecordServiceImpl.java @@ -0,0 +1,67 @@ +package com.sonic.cow.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.cow.dao.VoiceChatRecordDao; +import com.sonic.cow.domain.entity.VoiceChatRecord; +import com.sonic.cow.enums.VoiceChatStatusEnum; +import com.sonic.cow.service.VoiceChatRecordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +/** + * 语音聊天记录 + * + * @author code + */ +@Slf4j +@Service +public class VoiceChatRecordServiceImpl extends ServiceImpl implements VoiceChatRecordService { + + @Override + public Long add(Long userId, Long aiId, String roomId, String taskId,String endpoint) { + VoiceChatRecord voiceChatRecord = new VoiceChatRecord(); + voiceChatRecord.setUserId(userId); + voiceChatRecord.setAiId(aiId); + voiceChatRecord.setRoomId(roomId); + voiceChatRecord.setTaskId(taskId); + voiceChatRecord.setStatus(VoiceChatStatusEnum.CALLING.getCode()); + voiceChatRecord.setEndpoint(endpoint); + voiceChatRecord.setCreateTime(LocalDateTime.now()); + voiceChatRecord.setEditTime(LocalDateTime.now()); + save(voiceChatRecord); + return voiceChatRecord.getId(); + } + + @Override + public void updateStatus(String roomId, String taskId, VoiceChatStatusEnum status) { + update(Wrappers.lambdaUpdate() + .set(VoiceChatRecord::getStatus, status.getCode()) + .eq(VoiceChatRecord::getRoomId, roomId) + .eq(VoiceChatRecord::getTaskId, taskId) + ); + } + + @Override + public void updateDuration(String roomId, String taskId, Long duration) { + update(Wrappers.lambdaUpdate() + .set(VoiceChatRecord::getDuration, duration) + .set(VoiceChatRecord::getEditTime, LocalDateTime.now()) + .set(VoiceChatRecord::getStatus, VoiceChatStatusEnum.STOP.getCode()) + .eq(VoiceChatRecord::getRoomId, roomId) + .eq(VoiceChatRecord::getTaskId, taskId) + ); + } + + @Override + public VoiceChatRecord getByRoomIdAndTaskId(String roomId, String taskId) { + return getOne(Wrappers.lambdaQuery().eq(VoiceChatRecord::getRoomId, roomId).eq(VoiceChatRecord::getTaskId, taskId).last("limit 1")); + } + + @Override + public VoiceChatRecord getByTaskId(String taskId) { + return getOne(Wrappers.lambdaQuery().eq(VoiceChatRecord::getTaskId, taskId).last("limit 1")); + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceChatServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceChatServiceImpl.java new file mode 100644 index 0000000..cde49c3 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceChatServiceImpl.java @@ -0,0 +1,558 @@ +package com.sonic.cow.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessage; +import com.byteplus.ark.runtime.model.completion.chat.ChatMessageRole; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.sonic.cow.client.ResponseChatClient; +import com.sonic.cow.client.VoiceChatClient; +import com.sonic.cow.client.input.voicechat.domain.AgentConfig; +import com.sonic.cow.client.input.voicechat.domain.Audio; +import com.sonic.cow.client.input.voicechat.domain.UserPrompt; +import com.sonic.cow.client.request.ContentRequest; +import com.sonic.cow.client.request.InputRequest; +import com.sonic.cow.client.response.ResponseChatResponse; +import com.sonic.cow.client.rtc.callback.conversation.Conv; +import com.sonic.cow.client.rtc.callback.conversation.StageCode; +import com.sonic.cow.client.rtc.callback.rts.Data; +import com.sonic.cow.client.rtc.callback.rts.Subv; +import com.sonic.cow.domain.entity.PromptConfig; +import com.sonic.cow.domain.entity.VoiceChatRecord; +import com.sonic.cow.domain.input.GenerateRtcTokenInput; +import com.sonic.cow.domain.input.VoiceChatOptInput; +import com.sonic.cow.domain.output.GenerateRtcTokenOutput; +import com.sonic.cow.domain.output.VoiceChatOptOutput; +import com.sonic.cow.enums.*; +import com.sonic.cow.event.inner.payload.EmotionScorePayload; +import com.sonic.cow.event.outer.payload.UserDeductionStatPayload; +import com.sonic.cow.service.*; +import com.sonic.cow.utils.Constant; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.cow.utils.TimerUtils; +import com.sonic.frog.lib.client.AiChatInfoClient; +import com.sonic.frog.lib.client.AiClient; +import com.sonic.frog.lib.enums.HeartbeatLevelEnum; +import com.sonic.frog.lib.output.AiChatInfoOutput; +import com.sonic.frog.lib.output.AiInfoApiOutput; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.enums.ImMessageTypeEnum; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import com.sonic.pigeon.lib.output.HistoryMessageOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @description: 语音通话实现类 + * @author: mzc + * @date: 2025-07-30 11:24 + **/ +@Slf4j +@Service +public class VoiceChatServiceImpl implements VoiceChatService { + @Autowired + private VoiceChatClient voiceChatClient; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private VoiceChatRecordService voiceChatRecordService; + @Autowired + private AiClient aiClient; + @Autowired + private InputRequestBuildService inputRequestBuildService; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private AiChatInfoClient aiChatInfoClient; + @Autowired + private VoiceService voiceService; + @Autowired + private ContextSessionCacheService contextSessionCacheService; + @Autowired + private ResponseChatClient responseChatClient; + @Autowired + private PromptConfigService promptConfigService; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private ImMessageClient imMessageClient; + + + @Override + public GenerateRtcTokenOutput generateRtcToken(Long currentUserId, GenerateRtcTokenInput input) { + return GenerateRtcTokenOutput.builder() + .token(voiceChatClient.generateRtcToken(input.getRoomId(), currentUserId.toString())) + .build(); + } + + @Override + public VoiceChatOptOutput voiceChatOpt(Long currentUserId, VoiceChatOptInput input) { + VoiceChatOptTypeEnum optType = input.getOptType(); + switch (optType) { + case START: + startVoiceChat(currentUserId, input); + break; + case INTERRUPT: + interruptVoiceChat(currentUserId, input); + break; + case STOP: + stopVoiceChat(currentUserId, input); + break; + case CANCEL: + cancelVoiceChat(currentUserId, input); + break; + } + return null; + } + + /** + * 开始语音通话 + * + * @param currentUserId + * @param input + */ + private void startVoiceChat(Long currentUserId, VoiceChatOptInput input) { + //调用开启语音通话,先关闭用户之前的语音通话记录 + stopVoiceChat(currentUserId); + //TODO 步骤1:检测余额是否不足 + long startTime = TimerUtils.start(); + //检测余额是否不足 + boolean balanceIsInsufficient = voiceService.checkBalanceIsInsufficient(currentUserId, Constant.VOICE_CALL_DEDUCTION_AMOUNT); + if (balanceIsInsufficient) { + //余额不足,发送MQ,余额不足处理 + commonSendMqService.userBalanceInsufficientCheckoutMq(currentUserId); + ToastResultCode.USER_BALANCE_INSUFFICIENT.check(true); + } + TimerUtils.logCost(startTime, "步骤1:检测余额是否不足"); + + //TODO 步骤2: 获取ai基础信息 + startTime = TimerUtils.start(); + //真人用户id,ai用户id + String targetUserId = currentUserId.toString(); + String aiUserId = targetUserId + "@" + input.getAiId(); + Long aiId = input.getAiId(); + //通过aiId获取对应ai的配置 + AiInfoApiOutput aiInfo = aiClient.getAiInfo(aiId); + ToastResultCode.AI_NOT_EXIST.check(aiInfo == null); + TimerUtils.logCost(startTime, "步骤2: 获取ai基础信息"); + + //TODO 步骤3: 获取当前用户与ai的信息 + startTime = TimerUtils.start(); + //检测心动等级达到L4级才开启语音通话 + AiChatInfoOutput aiChatInfoOutput = aiChatInfoClient.getAiChatInfo(currentUserId, aiId); + List unlockHearbeatLevelList = aiChatInfoOutput.getUnlockHearbeatLevelList(); + ToastResultCode.SYS_PERMISSION_DENIED.check(CollectionUtils.isEmpty(unlockHearbeatLevelList)); + ToastResultCode.SYS_PERMISSION_DENIED.check(!unlockHearbeatLevelList.contains(HeartbeatLevelEnum.LEVEL_4)); + //语音类型,语速,音高 + Audio ttsAudio = Audio.builder() + .voice_type(StringUtils.isNotEmpty(aiInfo.getVoiceType()) ? aiInfo.getVoiceType() : "zh_female_cancan_mars_bigtts") + //TODO 临时使用 +// .voice_type("zh_female_cancan_mars_bigtts") + .speech_rate(StringUtils.isNotEmpty(aiInfo.getDialogueSpeechRate()) ? Integer.valueOf(aiInfo.getDialogueSpeechRate()) : 0) + .pitch_rate(StringUtils.isNotEmpty(aiInfo.getDialoguePitch()) ? Integer.valueOf(aiInfo.getDialoguePitch()) : 0) + .build(); + TimerUtils.logCost(startTime, "步骤3: 获取当前用户与ai的信息"); + + //TODO 步骤4:获取语音通话系统提示词 + startTime = TimerUtils.start(); + AiInfoApiOutput aiUserInfo = aiClient.getAiInfo(input.getAiId()); + //系统提示 + String systemPrompt = inputRequestBuildService.buildVoiceChatSystemPromptContent(currentUserId, aiId, aiUserInfo); + log.info("startVoiceChat systemPrompt:{}", systemPrompt); + TimerUtils.logCost(startTime, "步骤4:获取语音通话系统提示词"); + + //TODO 步骤5:获取用户与ai聊天历史记录 + startTime = TimerUtils.start(); + //用户与ai聊天历史记录 + List historyMessageList = inputRequestBuildService.getHistoryMessageList(currentUserId, aiId, 50); + //构建语音通话聊天历史 + List userPromptList = inputRequestBuildService.buildVoiceChatUserPrompts(currentUserId, aiId, historyMessageList); + log.info("startVoiceChat userPromptList:{}", JSON.toJSONString(userPromptList)); + TimerUtils.logCost(startTime, "步骤5:获取用户与ai聊天历史记录"); + + //TODO 步骤6:根据聊天上下文生成开启语音对话开场白 + startTime = TimerUtils.start(); + //根据聊天上下文生成开启语音对话开场白 + String dialoguePrologue = genStartVoiceChatDialoguePrologue(currentUserId, aiId, systemPrompt, historyMessageList); + TimerUtils.logCost(startTime, "步骤6:根据聊天上下文生成开启语音对话开场白"); + + //TODO 步骤7:调字节接口开启语音通话 + startTime = TimerUtils.start(); + log.info("startVoiceChat dialoguePrologue:{}", dialoguePrologue); + String welcomeMessage = StringUtils.isNotEmpty(dialoguePrologue) ? dialoguePrologue : aiInfo.getDialoguePrologue(); + log.info("startVoiceChat welcomeMessage:{}", welcomeMessage); + AgentConfig agentConfig = AgentConfig.builder() + .TargetUserID(new String[]{targetUserId}) + .UserId(aiUserId) + .WelcomeMessage(welcomeMessage) + .EnableConversationStateCallback(true) + .ServerMessageURLForRTS("https://test-cow.crushlevel.ai/web/voice-chat/conversation-state-callback") + .ServerMessageSignatureForRTS("123456") + .build(); + //调用启动聊天通话 + voiceChatClient.startVoiceChat(input.getRoomId(), input.getTaskId(), ttsAudio, systemPrompt, userPromptList, agentConfig); + TimerUtils.logCost(startTime, "步骤7:调字节接口开启语音通话"); + + //TODO 步骤8:保存语音通话记录 + startTime = TimerUtils.start(); + //保存聊天通话记录 + Long voiceCallId = voiceChatRecordService.add(currentUserId, aiId, input.getRoomId(), input.getTaskId(), input.getEndpoint()); + //设置语音通话双方与房间关联的任务id,RTC回调的时候用到 STOP时删除,缓存1天,避免用户在1天之后,没有结束通话,导致无法删除 + stringRedisTemplate.opsForValue().set(redisKeyUtils.voiceChatTaskIdKey(targetUserId), input.getTaskId(), 1, TimeUnit.DAYS); + stringRedisTemplate.opsForValue().set(redisKeyUtils.voiceChatTaskIdKey(aiUserId), input.getTaskId(), 1, TimeUnit.DAYS); + //发送到MQ计算费用 + commonSendMqService.voiceCallDeductionSendMq(currentUserId, aiId, voiceCallId); + TimerUtils.logCost(startTime, "步骤8:保存语音通话记录"); + } + + /** + * 获取启动语音通话的对话开场白 + * + * @param fromUserId + * @param toUserId + * @return + */ + private String genStartVoiceChatDialoguePrologue(Long fromUserId, Long toUserId, String systemPrompt, List historyMessageList) { + //获取模板 + PromptConfig systemPromptConfig = promptConfigService.getPromptTemplate(PromptTypeEnum.START_VOICE_CHAT_DIALOGUE_PROLOGUE.name()); + //获取上下文缓存 这个缓存一定是存在的 + String responseId = contextSessionCacheService.getByRedis(fromUserId, toUserId); + List inputRequests = Lists.newArrayList(); + if (StringUtils.isEmpty(responseId)) { + //获取最近10条聊天历史 + List last10HistoryMessageList = getLast10HistoryMessageList(historyMessageList); + //调用公共方法,组装入参数据 + inputRequestBuildService.buildStartVoiceChatDialoguePrologue(fromUserId, toUserId, inputRequests, systemPrompt, last10HistoryMessageList); + //构造系统提示词,并放到第一个位置 + InputRequest systemMessage = InputRequest.builder() + .role(ChatMessageRole.SYSTEM) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(systemPrompt).build())).build(); + inputRequests.add(0, systemMessage); + } + //构造用户提示词 + inputRequests.add(InputRequest.builder() + .role(ChatMessageRole.USER) + .content(Lists.newArrayList(ContentRequest.builder().type("input_text").text(systemPromptConfig.getPromptTemplate()).build())).build()); + //执行聊天 不将添加缓存 + //TODO 步骤6-1:模型生成开场白 + long startTime = TimerUtils.start(); + ResponseChatResponse chatResponse = responseChatClient.responseStructuredChat(responseId, null, inputRequests, null, false); + TimerUtils.logCost(startTime, "步骤6-1 模型生成开场白"); + + log.info("===> genStartVoiceChatDialoguePrologue chatResponse : {}", chatResponse); + if (chatResponse != null) { + if (chatResponse.getMessage() != null) { + String message = chatResponse.getMessage().replaceAll("(.*?)", ""); + message = message.replaceAll("\\(.*?\\)", ""); + return message; + } + } + return null; + } + + /** + * 获取最近10条聊天历史 + * + * @param historyMessageList + * @return + */ + private List getLast10HistoryMessageList(List historyMessageList) { + // 1. 处理空列表(避免空指针) + if (historyMessageList == null || historyMessageList.isEmpty()) { + return new ArrayList<>(); // 或返回 Collections.emptyList()(不可修改) + } + + // 2. 计算起始索引:总长度 - 10,若结果 < 0 则取 0(不足 10 条) + int totalSize = historyMessageList.size(); + int startIndex = Math.max(0, totalSize - 10); + + // 3. 截取后 10 条(subList 是原列表的视图,若需独立列表可转成新 ArrayList) + return new ArrayList<>(historyMessageList.subList(startIndex, totalSize)); + } + + /** + * 打断语音通话 + * + * @param currentUserId + * @param input + */ + private void interruptVoiceChat(Long currentUserId, VoiceChatOptInput input) { + //打断 + voiceChatClient.updateVoiceChat(input.getRoomId(), input.getTaskId()); + //更新通话状态为打断 + voiceChatRecordService.updateStatus(input.getRoomId(), input.getTaskId(), VoiceChatStatusEnum.INTERRUPT); + } + + /** + * 结束语音通话 + * + * @param currentUserId + * @param input + */ + private void stopVoiceChat(Long currentUserId, VoiceChatOptInput input) { + //真人用户id,ai用户id + String targetUserId = currentUserId.toString(); + String aiUserId = targetUserId + "@" + input.getAiId(); + //查询房间是否已经结束,如果已经结束则快速返回 + VoiceChatRecord voiceChatRecord = voiceChatRecordService.getByRoomIdAndTaskId(input.getRoomId(), input.getTaskId()); + if (voiceChatRecord == null || voiceChatRecord.getStatus() == 3) { + return; + } + //结束聊天通话 + voiceChatClient.stopVoiceChat(input.getRoomId(), input.getTaskId()); + //记录通话时长 + voiceChatRecordService.updateDuration(input.getRoomId(), input.getTaskId(), input.getDuration()); + //获取语音通话最后对话轮次 + String cacheRoundId = stringRedisTemplate.opsForValue().get(redisKeyUtils.voiceChatRoundIdKey(input.getTaskId())); + if (cacheRoundId != null) { + //有对话,获取最后一次对话内容,发送MQ进行情绪打分,并进行结束处理 + getRoundDataAndScore(input.getTaskId(), cacheRoundId, true, input.getDuration()); + } else { + //没有对话,结束处理 + voiceCallEndHandler(currentUserId, input.getAiId(), null, input.getDuration()); + } + //删除设置语音通话双方关联的任务id + stringRedisTemplate.delete(redisKeyUtils.voiceChatTaskIdKey(targetUserId)); + stringRedisTemplate.delete(redisKeyUtils.voiceChatTaskIdKey(aiUserId)); + //删除缓存 对话轮次数据,缓存数据 + stringRedisTemplate.delete(redisKeyUtils.voiceChatRoundIdKey(input.getTaskId())); + stringRedisTemplate.delete(redisKeyUtils.voiceChatRoundDataKey(input.getTaskId())); + //发送到用户预扣费统计队列 + userDeductionStatSendMq(input.getRoomId(), input.getTaskId()); + } + + /** + * 取消语音通话 + * + * @param currentUserId + * @param input + */ + private void cancelVoiceChat(Long currentUserId, VoiceChatOptInput input) { + voiceChatClient.stopVoiceChat(input.getRoomId(), input.getTaskId()); + } + + + /** + * 语音通话结束,发送用户预扣费统计队列,捐款,生成流水 + * + * @param roomId + * @param taskId + */ + private void userDeductionStatSendMq(String roomId, String taskId) { + VoiceChatRecord voiceChatRecord = voiceChatRecordService.getByRoomIdAndTaskId(roomId, taskId); + if (voiceChatRecord == null) { + return; + } + HashMap extraMap = Maps.newHashMap(); + extraMap.put("voiceCallId", voiceChatRecord.getId()); + extraMap.put("duration", voiceChatRecord.getDuration()); + extraMap.put("status", voiceChatRecord.getStatus()); + commonSendMqService.userDeductionStatSendMq(UserDeductionStatPayload.builder() + .userId(voiceChatRecord.getUserId()) + .aiId(voiceChatRecord.getAiId()) + .deductionType(DeductionTypeEnum.VOICE_CALL) + .bizTime(LocalDateTime.now()) + .extra(JSON.toJSONString(extraMap)) + .build()); + } + + @Override + public void webhook(HttpServletRequest request, HttpServletResponse response) { + voiceChatClient.webhook(request, response); + } + + @Deprecated + @Override + public void conversationStateCallback(HttpServletRequest request, HttpServletResponse response) { + Conv conv = voiceChatClient.conversationStateCallback(request, response); + log.info("conversationStateCallback conv:{}", conv); + if (conv != null && conv.getStage() != null) { + //智能体完成说话,发送MQ计算心动等级 + if (StageCode.ANSWER_FINISH.getCode() == conv.getStage().getCode()) { + String taskId = conv.getTaskId(); + } + } + } + + @Override + public void rtsCallback(HttpServletRequest request, HttpServletResponse response) { + Subv subv = voiceChatClient.rtsCallback(request, response); + if (subv == null) { + return; + } + List dataList = subv.getData(); + log.info("rtsCallback dataList:{}", dataList); + Data data = dataList.get(0); + if (data != null && data.getRoundId() != null && data.getRoundId() > 0) { + //开启语音通话时,给值 + String taskId = stringRedisTemplate.opsForValue().get(redisKeyUtils.voiceChatTaskIdKey(data.getUserId())); + log.info("rtsCallback taskId:{}", taskId); + if (taskId == null) { + return; + } + //当前轮次与cache轮次不一样,说明上一轮次对方完,获取对话上一轮次语音内容并打分 + String cacheRoundId = stringRedisTemplate.opsForValue().get(redisKeyUtils.voiceChatRoundIdKey(taskId)); + if (cacheRoundId != null && !cacheRoundId.equals(data.getRoundId().toString())) { + getRoundDataAndScore(taskId, cacheRoundId, false, null); + } + //将每次对话内容保存到redis Hash中,key为roundId,value为每次用户对话内容 结构为 Map> + cacheRoundData(taskId, data); + //设置对话轮次 + stringRedisTemplate.opsForValue().set(redisKeyUtils.voiceChatRoundIdKey(taskId), data.getRoundId().toString()); + } + } + + /** + * 获取对话轮次语音内容 + * + * @param taskId + * @param cacheRoundId + */ + private void getRoundDataAndScore(String taskId, String cacheRoundId, boolean isEnd, Long duration) { + //对话轮次不一致的话,拼成cacheRoundId对应的用户与AI的完整内容 注:最后一次需要在停止语音通话时,将cacheRoundId对应的用户与AI的完整内容拼成完整内容,并保存到数据库中 + String cacheRoundDataStr = (String) stringRedisTemplate.opsForHash().get(redisKeyUtils.voiceChatRoundDataKey(taskId), cacheRoundId.toString()); + if (StringUtils.isNotEmpty(cacheRoundDataStr)) { + Map> userDataMap = JSON.parseObject(cacheRoundDataStr, new TypeReference>>() { + }); + + List messageList = Lists.newArrayList(); + for (Map.Entry> userEntry : userDataMap.entrySet()) { + //用户id + String userId = userEntry.getKey(); + //用户对应的语音内容 + List dataList = userEntry.getValue(); + //以升序排序 + dataList.sort(Comparator.comparingInt(Data::getSequence)); + //获取用户对应轮次的完整语音内容 + String content = dataList.stream().map(Data::getText).collect(Collectors.joining()); + //不为空的时候才拼成消息记录 + if (StringUtils.isNotEmpty(content)) { + messageList.add(ChatMessage.builder() + .role(userId.contains("@") ? ChatMessageRole.ASSISTANT : ChatMessageRole.USER) + .content(content) + .build()); + } + } + //打印拼出的轮次的完整语音内容 + log.info("getRoundDataAndScore round:{},messageList: {}", cacheRoundId, JSON.toJSONString(messageList)); + //发送MQ,调用模型让模型计算情绪得分 消息数成对 + if (CollectionUtils.isNotEmpty(messageList)) { + //倒序一下,不然ai说的话在前面,用户说在后面, + Collections.reverse(messageList); + //发送MQ,语音通话情绪得分 + //taskId 实际由用户id-aiId-时间戮组成 + String[] split = taskId.split("-"); + Long voiceUserId = Long.valueOf(split[0]); + Long voiceAiId = Long.valueOf(split[1]); + commonSendMqService.emotionScoreSendMq(EmotionScorePayload.builder() + .chatMessageList(messageList) + .userId(voiceUserId) + .aiId(voiceAiId) + .isEnd(isEnd) + .duration(duration).build()); + } + } + } + + /** + * 缓存对话轮次数据 + * + * @param taskId + * @param data + */ + private void cacheRoundData(String taskId, Data data) { + //获取对话轮次数据 + String voiceChatRoundDataKey = redisKeyUtils.voiceChatRoundDataKey(taskId); + String roundDataStr = (String) stringRedisTemplate.opsForHash().get(voiceChatRoundDataKey, data.getRoundId().toString()); + //本对话轮次用户数据Map + Map> userDataMap = new HashMap<>(); + if (StringUtils.isEmpty(roundDataStr)) { + //没有,初始化 + userDataMap.put(data.getUserId(), Lists.newArrayList(data)); + } else { + //存在,更新 + userDataMap = JSON.parseObject(roundDataStr, new TypeReference>>() { + }); + //该轮次对应用户有没有数据 + List userData = userDataMap.get(data.getUserId()); + if (CollectionUtils.isNotEmpty(userData)) { + //有追加 + userData.add(data); + } else { + //没有,初始化 + userData = Lists.newArrayList(data); + } + userDataMap.put(data.getUserId(), userData); + } + //保存本轮次语音内容到缓存中 + stringRedisTemplate.opsForHash().put(voiceChatRoundDataKey, data.getRoundId().toString(), JSON.toJSONString(userDataMap)); + //设置对话轮次缓存有效期 + stringRedisTemplate.expire(voiceChatRoundDataKey, 1, TimeUnit.DAYS); + } + + @Override + public void voiceCallEndHandler(Long userId, Long aiId, List voiceChatInputRequestList, Long duration) { + //发送IM自定义消息 + Map extra = new HashMap<>(); + extra.put("type", ImMessageTypeEnum.CALL.name()); + if (CollectionUtils.isNotEmpty(voiceChatInputRequestList) && voiceChatInputRequestList.size() > 1) { + extra.put("voiceChatInputRequestList", JSON.toJSONString(voiceChatInputRequestList)); + } + extra.put("callType", "CALL_END"); + extra.put("duration", duration); + SendAiCustomerMessageInput sendAiCustomerMessageInput = new SendAiCustomerMessageInput(); + sendAiCustomerMessageInput.setFromUserId(aiId); + sendAiCustomerMessageInput.setToUserId(userId); + sendAiCustomerMessageInput.setContent("语音通话"); + sendAiCustomerMessageInput.setAttachment(JSONObject.toJSONString(extra)); + imMessageClient.sendAiToUserCustomerMessage(sendAiCustomerMessageInput); + //删除responseId缓存 + if (CollectionUtils.isNotEmpty(voiceChatInputRequestList)) { + contextSessionCacheService.deleteRedis(userId, aiId); + } + } + + @Override + public Boolean stopVoiceChat(Long userId) { + //通过用户id获取taskId + String taskId = stringRedisTemplate.opsForValue().get(redisKeyUtils.voiceChatTaskIdKey(userId.toString())); + log.info("stopVoiceChat taskId:{}", taskId); + //如果删除了,则直接返回 + if (taskId == null) { + return true; + } + VoiceChatRecord voiceChatRecord = voiceChatRecordService.getByTaskId(taskId); + log.info("stopVoiceChat voiceChatRecord:{}", JSON.toJSONString(voiceChatRecord)); + //已经结束,直接return + if (voiceChatRecord.getStatus() == 3) { + return true; + } + Long startTime = voiceChatRecord.getCreateTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + Long duration = (System.currentTimeMillis() - startTime); + //调用stopVoiceChat接口,停止语音通话 + VoiceChatOptInput voiceChatOptInput = VoiceChatOptInput.builder() + .aiId(voiceChatRecord.getAiId()) + .roomId(voiceChatRecord.getRoomId()) + .optType(VoiceChatOptTypeEnum.STOP) + .taskId(taskId) + .duration(duration) + .build(); + log.info("stopVoiceChat voiceChatOptInput:{} ", JSON.toJSONString(voiceChatOptInput)); + voiceChatOpt(voiceChatRecord.getUserId(), voiceChatOptInput); + return true; + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceServiceImpl.java new file mode 100644 index 0000000..c180246 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceServiceImpl.java @@ -0,0 +1,158 @@ +package com.sonic.cow.service.impl; + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.Maps; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.client.AsrV2Client; +import com.sonic.cow.client.TtsV3Client; +import com.sonic.cow.domain.input.VoiceAsrInput; +import com.sonic.cow.domain.input.VoiceTtsV2Input; +import com.sonic.cow.domain.output.AsrOutput; +import com.sonic.cow.enums.DeductionTypeEnum; +import com.sonic.cow.enums.ToastResultCode; +import com.sonic.cow.event.outer.payload.UserDeductionStatPayload; +import com.sonic.cow.service.CommonSendMqService; +import com.sonic.cow.service.VoiceService; +import com.sonic.cow.utils.LimitUtils; +import com.sonic.cow.utils.RedisKeyUtils; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.output.AccountBuffOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.HashMap; + +import static com.sonic.cow.utils.Constant.VOICE_DEDUCTION_AMOUNT; + +/** + * @description: 语音通话实现类 + * @author: mzc + * @date: 2025-07-30 11:24 + **/ +@Slf4j +@Service +public class VoiceServiceImpl implements VoiceService { + + @Autowired + private AsrV2Client asrV2Client; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private PayClient payClient; + @Autowired + private LimitUtils limitUtils; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private TtsV3Client ttsV3Client; + + @Override + public AsrOutput asr(Long userId, VoiceAsrInput input) throws UnirestException, InterruptedException { + //检测余额是否不足 + boolean balanceIsInsufficient = checkBalanceIsInsufficient(userId, VOICE_DEDUCTION_AMOUNT); + if (balanceIsInsufficient) { + //余额不足,发送MQ,余额不足处理 + commonSendMqService.userBalanceInsufficientCheckoutMq(userId); + ToastResultCode.USER_BALANCE_INSUFFICIENT.check(true); + } + //调用语音识别服务 + AsrOutput output = asrV2Client.asr(userId.toString(), input); + //发送语音预扣费统计MQ + input.setAiId(input.getAiId() != null ? input.getAiId() : userId); + HashMap extraMap = Maps.newHashMap(); + extraMap.put("url", input.getUrl()); + extraMap.put("asr", true); + //异步扣费处理 + userDeductionStatSendMq(userId, input.getAiId(), extraMap); + return output; + } + + @Override + public String tts(Long userId, VoiceTtsV2Input input) { + String tts = ""; + if (input.getAiId() != null) { + //检测余额是否不足 + boolean balanceIsInsufficient = checkBalanceIsInsufficient(userId, VOICE_DEDUCTION_AMOUNT); + if (balanceIsInsufficient) { + //余额不足,发送MQ,余额不足处理 + commonSendMqService.userBalanceInsufficientCheckoutMq(userId); + ToastResultCode.USER_BALANCE_INSUFFICIENT.check(true); + } + //调用文本转语音服务 + tts = ttsV3Client.tts(userId.toString(), input); + //发送语音使用统计MQ + input.setAiId(input.getAiId() != null ? input.getAiId() : userId); + HashMap extraMap = Maps.newHashMap(); + extraMap.put("text", input.getText()); + extraMap.put("tts", true); + userDeductionStatSendMq(userId, input.getAiId(), extraMap); + } else { + //创建,编辑语音合成,1分钟内20次的限制 + boolean b = limitUtils.defaultLimitCheckByKey(redisKeyUtils.ttsMinuteLimitKey(userId), 20, 60); + ToastResultCode.REQUEST_LIMIT_ERROR.check(b); + //调用文本转语音服务 + tts = ttsV3Client.tts(userId.toString(), input); + } + return tts; + } + + @Override + public void ttsDeAmount(Long userId, Long aiId) { + if(aiId == null) { + //做一个限流,防刷(24小时内最多只能有20次免费调用次数) + boolean bl = limitUtils.defaultLimitCheckByKey(redisKeyUtils.ttsDeAmountLimitKey(userId), 20, 60 * 60 * 24); + ToastResultCode.USER_BALANCE_INSUFFICIENT.check(bl); + return; + } + //检测余额是否不足 + boolean balanceIsInsufficient = checkBalanceIsInsufficient(userId, VOICE_DEDUCTION_AMOUNT); + if (balanceIsInsufficient) { + //余额不足,发送MQ,余额不足处理 + commonSendMqService.userBalanceInsufficientCheckoutMq(userId); + ToastResultCode.USER_BALANCE_INSUFFICIENT.check(true); + } + } + + + /** + * 发送用户语音使用统计MQ + * + * @param userId + * @param aiId + * @param extraMap + */ + private void userDeductionStatSendMq(Long userId, Long aiId, HashMap extraMap) { + commonSendMqService.userDeductionStatSendMq(UserDeductionStatPayload.builder() + .userId(userId) + .aiId(aiId) + .deductionType(DeductionTypeEnum.VOICE) + .bizTime(LocalDateTime.now()) + .extra(JSON.toJSONString(extraMap)) + .build()); + } + + + /** + * 检测余额是否不足 + * + * @param userId + * @param amount 本次金额 + * @return + */ + @Override + public boolean checkBalanceIsInsufficient(Long userId, Long amount) { + try { + AccountBuffOutput accountBuff = payClient.getAccountBuff(userId); + Long balance = accountBuff != null && accountBuff.getBalance() > 0 ? accountBuff.getBalance() : 0; + log.info("checkBalanceIsInsufficient balance:{},totalDeductionAmount:{}", balance, amount); + if (balance < amount) { + return true; + } + } catch (Exception e) { + log.error("checkBalanceIsInsufficient error:", e); + } + return false; + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceV2ServiceImpl.java b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceV2ServiceImpl.java new file mode 100644 index 0000000..0e738e2 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/service/impl/VoiceV2ServiceImpl.java @@ -0,0 +1,112 @@ +package com.sonic.cow.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Maps; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.domain.output.AsrOutput; +import com.sonic.cow.enums.DeductionTypeEnum; +import com.sonic.cow.enums.ToastResultCode; +import com.sonic.cow.event.outer.payload.UserDeductionStatPayload; +import com.sonic.cow.service.CommonSendMqService; +import com.sonic.cow.service.VoiceV2Service; +import com.sonic.cow.utils.KeyGenerator; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.enums.BizType; +import com.sonic.lion.lib.input.BalanceCheckoutInput; +import com.sonic.lion.lib.output.AccountBuffOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.time.LocalDateTime; +import java.util.HashMap; + +import static com.sonic.cow.utils.Constant.VOICE_DEDUCTION_AMOUNT; + +/** + * @description: 语音通话实现类 + * @author: mzc + * @date: 2025-07-30 11:24 + **/ +@Slf4j +@Service +public class VoiceV2ServiceImpl implements VoiceV2Service { + + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private PayClient payClient; + + @Override + public AsrOutput asr(Long userId, Long aiId, File file) throws UnirestException { + //检测余额是否不足 + boolean balanceIsInsufficient = checkBalanceIsInsufficient(userId, VOICE_DEDUCTION_AMOUNT); + if (balanceIsInsufficient) { + //余额不足,发送MQ,余额不足处理 + commonSendMqService.userBalanceInsufficientCheckoutMq(userId); + ToastResultCode.USER_BALANCE_INSUFFICIENT.check(true); + } + HttpResponse response = Unirest.post("https://api.openai.com/v1/audio/transcriptions") + .header("Authorization", "Bearer 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") + .field("file", file) + .field("model", "gpt-4o-mini-transcribe") + .asString(); + String result = response.getBody(); + log.info("===> asr result:{}", result); + + HashMap extraMap = Maps.newHashMap(); + extraMap.put("asr", true); + //异步扣费处理 + userDeductionStatSendMq(userId, aiId, extraMap); + + JSONObject jsonObject = JSONObject.parseObject(result); + AsrOutput asrOutput = new AsrOutput(); + asrOutput.setContent(jsonObject.getString("text")); + return asrOutput; + } + + + /** + * 发送用户语音使用统计MQ + * + * @param userId + * @param aiId + * @param extraMap + */ + private void userDeductionStatSendMq(Long userId, Long aiId, HashMap extraMap) { + commonSendMqService.userDeductionStatSendMq(UserDeductionStatPayload.builder() + .userId(userId) + .aiId(aiId) + .deductionType(DeductionTypeEnum.VOICE) + .bizTime(LocalDateTime.now()) + .extra(JSON.toJSONString(extraMap)) + .build()); + } + + /** + * 检测余额是否不足 + * + * @param userId + * @param amount 本次金额 + * @return + */ + @Override + public boolean checkBalanceIsInsufficient(Long userId, Long amount) { + try { + AccountBuffOutput accountBuff = payClient.getAccountBuff(userId); + Long balance = accountBuff != null && accountBuff.getBalance() > 0 ? accountBuff.getBalance() : 0; + log.info("checkBalanceIsInsufficient balance:{},totalDeductionAmount:{}", balance, amount); + if (balance < amount) { + return true; + } + } catch (Exception e) { + log.error("checkBalanceIsInsufficient error:", e); + } + return false; + } + +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/tools/FunctionParameterSchema.java b/sonic-cow/server/src/main/java/com/sonic/cow/tools/FunctionParameterSchema.java new file mode 100644 index 0000000..50449e8 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/tools/FunctionParameterSchema.java @@ -0,0 +1,21 @@ +package com.sonic.cow.tools; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class FunctionParameterSchema { + + public String type; + public Map properties; + public List required; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/tools/GetAiImageArgs.java b/sonic-cow/server/src/main/java/com/sonic/cow/tools/GetAiImageArgs.java new file mode 100644 index 0000000..32bd4d5 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/tools/GetAiImageArgs.java @@ -0,0 +1,21 @@ +package com.sonic.cow.tools; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GetAiImageArgs { + + @JsonProperty("userId") + private Long userId; + + @JsonProperty("aiId") + private Long aiId; + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/tools/ToolsDefinition.java b/sonic-cow/server/src/main/java/com/sonic/cow/tools/ToolsDefinition.java new file mode 100644 index 0000000..a4faf37 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/tools/ToolsDefinition.java @@ -0,0 +1,41 @@ +package com.sonic.cow.tools; + +import com.google.common.collect.Lists; +import com.sonic.cow.client.request.Tools; + +import java.util.HashMap; +import java.util.Map; + +/** + * 函数定义 + */ +public class ToolsDefinition { + + public static Tools getAiImageToolsV2(String functionName, String functionDescription) { + // 步骤 1: 定义工具 + Map userIdProperty = new HashMap<>(); + userIdProperty.put("type", "long"); + userIdProperty.put("description", "userId"); + + Map aiIdProperty = new HashMap<>(); + aiIdProperty.put("type", "long"); + aiIdProperty.put("description", "aiId"); + + Map schemaProperties = new HashMap<>(); + schemaProperties.put("userId", userIdProperty); + schemaProperties.put("aiId", aiIdProperty); + + FunctionParameterSchema functionParams = new FunctionParameterSchema( + "object", + schemaProperties, + Lists.newArrayList("userId", "aiId") // 'userId'、'aiId' 是必需参数 + ); + return Tools.builder() + .type("function") + .name(functionName) + .parameters(functionParams.toString()) + .description(functionDescription) + .build(); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/AbstractKeyGenerator.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/AbstractKeyGenerator.java new file mode 100644 index 0000000..06f1636 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/AbstractKeyGenerator.java @@ -0,0 +1,77 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.sonic.cow.utils; + + + +import com.sonic.cow.enums.ToastResultCode; + +import java.net.InetAddress; +import java.text.NumberFormat; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class AbstractKeyGenerator { + private static final int SEQ_MIN = 1; + private static final int SEQ_MAX = 999; + private static final int SEQ_DIGITS = 3; + private static final AtomicInteger SEQ_ATOMIC_INTEGER = new AtomicInteger(1); + private static final char IP_SPACER = '.'; + private static final String IP_AFTER_TWO; + private static final int IP_DIGITS = 2; + private static final int IP_MOD = 99; + private static final String PATTERN = "yyyyMMddHHmmssSSS"; + private static final int ORDER_NO_MAX_SIZE = 29; + + public AbstractKeyGenerator() { + } + + public String generatorUniqueKey(String businessCode) { + StringBuffer buffer = new StringBuffer(); + String date = DateUtils.getNow(PATTERN); + buffer.append(this.customKey()).append(businessCode).append(date).append(IP_AFTER_TWO).append(formatNumber((long)getSeq(), SEQ_DIGITS)); + + ToastResultCode.SYS_SYSTEM_EXCEPTION.check(buffer.length() > ORDER_NO_MAX_SIZE, "tradeNo to long, tradeNo->" + buffer.toString()); + return buffer.toString(); + } + + public abstract String customKey(); + + private static int getSeq() { + int result = SEQ_ATOMIC_INTEGER.incrementAndGet(); + if (result <= SEQ_MAX) { + return result; + } else { + SEQ_ATOMIC_INTEGER.set(SEQ_MIN); + return SEQ_MIN; + } + } + + private static String formatNumber(long number, int digits) { + NumberFormat nf = NumberFormat.getInstance(); + nf.setMaximumIntegerDigits(digits); + nf.setMinimumIntegerDigits(digits); + nf.setGroupingUsed(false); + return nf.format(number); + } + + static { + String ip; + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + ip = inetAddress.getHostAddress(); + ip = ip.substring(ip.lastIndexOf(46) + 1); + ip = formatNumber(Long.valueOf(ip), IP_DIGITS); + } catch (Exception var4) { + Random random = new Random((long)UUID.randomUUID().toString().hashCode()); + int randomNum = random.nextInt(IP_MOD); + ip = formatNumber((long)randomNum, IP_DIGITS); + } + + IP_AFTER_TWO = ip; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/Base64Utils.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/Base64Utils.java new file mode 100644 index 0000000..208ac82 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/Base64Utils.java @@ -0,0 +1,71 @@ +package com.sonic.cow.utils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; + +public class Base64Utils { + + /** + * 将 Base64 字符串转换为文件 + * + * @param base64String Base64 编码的字符串(可带 data URL 前缀,如 "data:image/png;base64,") + * @param filePath 输出文件路径(包含文件名和扩展名,如 "/path/to/output.png") + * @return 生成的文件对象,如果失败返回 null + */ + public static File decodeToFile(String base64String, String filePath) { + if (base64String == null || base64String.trim().isEmpty()) { + throw new IllegalArgumentException("Base64 字符串不能为空"); + } + + // 去除 data URL 前缀(如果存在) + String cleanBase64 = base64String; + if (base64String.startsWith("data:")) { + int commaIndex = base64String.indexOf(','); + if (commaIndex != -1) { + cleanBase64 = base64String.substring(commaIndex + 1); + } + } + + byte[] decodedBytes; + try { + decodedBytes = Base64.getDecoder().decode(cleanBase64); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("无效的 Base64 字符串: " + e.getMessage(), e); + } + + // 创建文件和目录 + File outputFile = new File(filePath); + Path parentDir = outputFile.getParentFile().toPath(); + if (parentDir != null && !Files.exists(parentDir)) { + try { + Files.createDirectories(parentDir); + } catch (IOException e) { + throw new RuntimeException("创建目录失败: " + parentDir, e); + } + } + + // 写入文件 + try (FileOutputStream fos = new FileOutputStream(outputFile)) { + fos.write(decodedBytes); + } catch (IOException e) { + throw new RuntimeException("写入文件失败: " + filePath, e); + } + + return outputFile; + } + + /** + * 重载方法:使用临时文件(系统临时目录) + * + * @param base64String Base64 字符串 + * @param fileName 文件名(包含扩展名,如 "image.png") + * @return 生成的临时文件对象 + */ + public static File decodeToTempFile(String base64String, String fileName) { + return decodeToFile(base64String, "/path/to/" + fileName); + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/BeanConvert.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/BeanConvert.java new file mode 100644 index 0000000..29d46bb --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/BeanConvert.java @@ -0,0 +1,64 @@ +package com.sonic.cow.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cglib.beans.BeanCopier; + +import java.util.ArrayList; +import java.util.List; + +/** + * 对象拷贝 + * @author dev-center + */ +public class BeanConvert { + private final static Logger LOG = LoggerFactory.getLogger(BeanConvert.class); + + /** + * 实例类转换 + * + * @param source 源对象 + * @param target 目标对象 + * @param 源对象 + * @param 目标对象 + */ + public static K copeBean(T source, Class target) { + if (source == null) { + return null; + } + BeanCopier beanCopier = BeanCopier.create(source.getClass(), target, false); + K result = null; + try { + result = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(source, result, null); + return result; + } + + /** + * page实例类转换 + * + * @param 源对象 + * @param 目标对象 + * @param source 源对象 + * @param target 目标对象 + * @return + */ + public static List copeList(List source, Class target) { + List list = new ArrayList<>(); + for (T af : source) { + BeanCopier beanCopier = BeanCopier.create(af.getClass(), target, false); + K af1 = null; + try { + af1 = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(af, af1, null); + list.add(af1); + } + return list; + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/CacheUtils.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/CacheUtils.java new file mode 100644 index 0000000..e8a7fa9 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/CacheUtils.java @@ -0,0 +1,152 @@ +package com.sonic.cow.utils; + +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 统一的缓存工具类 + */ +@Component +public class CacheUtils { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * 清理String类型的缓存数据 + * @param redisKey + */ + public void clearStringCache(String redisKey) { + stringRedisTemplate.delete(redisKey); + } + + /** + * 获取指定类型的缓存对象数据 + * @param redisKey + * @param clazz + * @param + * @return + */ + public T getCacheObject(String redisKey, Class clazz) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKey); + if(StringUtils.isEmpty(cacheStr)) { + return null; + } + return JSONObject.parseObject(cacheStr, clazz); + } + + /** + * 获取指定类型的缓存列表对象数据 + * @param redisKey + * @param clazz + * @param + * @return + */ + public List getCacheList(String redisKey, Class clazz) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKey); + if(StringUtils.isEmpty(cacheStr)) { + return null; + } + return JSONObject.parseArray(cacheStr, clazz); + } + + /** + * 获取缓存对象数据,如果缓存数据不存在的话则设置缓存对象 + * @param redisKey + * @param clazz + * @param abstractCache + * @param timeSeconds + * @param + * @return + */ + public T getCacheObjectAndSet(String redisKey, Class clazz, AbstractCache abstractCache, int timeSeconds) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKey); + if(StringUtils.isNotEmpty(cacheStr)) { + return JSONObject.parseObject(cacheStr, clazz); + } + String param = abstractCache.getData(); + if(param == null) { + return null; + } + setCache(redisKey, param, timeSeconds); + return JSONObject.parseObject(param, clazz); + } + + /** + * 获取缓存对象数据,如果缓存数据不存在的话则设置缓存对象 + * @param redisKey + * @param abstractCache + * @param timeSeconds + * @return + */ + public String getCacheStringAndSet(String redisKey, AbstractCache abstractCache, int timeSeconds) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKey); + if(StringUtils.isNotEmpty(cacheStr)) { + return cacheStr; + } + String param = abstractCache.getData(); + if(param == null) { + return null; + } + setCache(redisKey, param, timeSeconds); + return param; + } + + /** + * 获取缓存列表数据,如果缓存数据不存在的话则设置缓存列表 + * @param redisKey + * @param clazz + * @param abstractCache + * @param timeSeconds + * @param + * @return + */ + public List getCacheListAndSet(String redisKey, Class clazz, AbstractCache abstractCache, int timeSeconds) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKey); + if(StringUtils.isNotEmpty(cacheStr)) { + return JSONObject.parseArray(cacheStr, clazz); + } + String param = abstractCache.getData(); + if(param == null) { + return null; + } + setCache(redisKey, param, timeSeconds); + return JSONObject.parseArray(param, clazz); + } + + /** + * 设置缓存数据 + * @param redisKey + * @param param + * @param timeSeconds + */ + public void setCache(String redisKey, String param, int timeSeconds) { + stringRedisTemplate.opsForValue().set(redisKey, param, timeSeconds, TimeUnit.SECONDS); + } + + /** + * 清理缓存数据 + * @param redisKey + */ + public void removeCache(String redisKey) { + stringRedisTemplate.delete(redisKey); + } + + + public interface AbstractCache { + + /** + * 获取需要存储到缓存的数据 + * @return + */ + String getData(); + + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/ChatResponseFormatUtils.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/ChatResponseFormatUtils.java new file mode 100644 index 0000000..0d9ab44 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/ChatResponseFormatUtils.java @@ -0,0 +1,28 @@ +package com.sonic.cow.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.byteplus.ark.runtime.model.completion.chat.ChatCompletionRequest; + +public class ChatResponseFormatUtils { + + + + /** + * 获取响应格式 + * @param jsonSchema + * @param name + * @param desc + * @return + * @throws Exception + */ + public static ChatCompletionRequest.ChatCompletionRequestResponseFormat getResponseFormat(String jsonSchema, String name, String desc) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode schemaNode = mapper.readTree(jsonSchema); + // 创建响应格式对象,指定返回数据应符合JSON Schema格式,用于定义结构化输出 type值只能为:text、json_schema、json_object + return new ChatCompletionRequest.ChatCompletionRequestResponseFormat("json_object", + new ResponseFormatJSONSchemaJSONSchemaParam(name, desc, schemaNode, true)); + } + + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/CheckUtils.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/CheckUtils.java new file mode 100644 index 0000000..fcd1725 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/CheckUtils.java @@ -0,0 +1,108 @@ +package com.sonic.cow.utils; + + + +import com.sonic.cow.domain.bo.LimitBo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * @Author code + * @Description TODO + * @Date 2024/3/7 13:37 + * @Version 1.0 + */ +public class CheckUtils { + + public static String[] BASE_CHARS = new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", + "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", + "T", "U", "V", "W", "X", "Y", "Z"}; + + /** + * 计算小时分钟后组装异常信息 + * @param bo + * @return + */ + public static String getErrorMessage(LimitBo bo, String message) { + if(!bo.getLimitBl()) { + return ""; + } + //计算得到总的分钟数 + Long expTime = bo.getExpTime() / 60; + //计算小时、分钟 + Long expHour = expTime / 60; + //计算得到剩余的分钟数 + Long expMinutes = expTime % 60; + expHour = expHour <= 0 ? 0 : expHour; + expMinutes = expMinutes <= 0 ? 0 : expMinutes; + if(expHour <= 0 && expMinutes <= 0) { + expMinutes = 1L; + } + return String.format(message, expHour, expMinutes); + } + + /** + * 生成有时间戳编码的30位的UUID + * @return + */ + public static String genUUIDAnEncode() { + //获取当前时间戳 + Long time = System.currentTimeMillis() / 1000; + //将时间戳按照每2个字符进行拆分 + List timeList = splitTimestamp(time); + //拼接出字符串的UUID,30位 + StringBuffer uuid = new StringBuffer(); + uuid.append(randomUUID(2)).append(timeList.get(0)); + uuid.append(randomUUID(2)).append(timeList.get(1)); + uuid.append(randomUUID(2)).append(timeList.get(2)); + uuid.append(randomUUID(2)).append(timeList.get(3)); + uuid.append(randomUUID(2)).append(timeList.get(4)); + return uuid.toString(); + } + + /** + * 反解出时间戳 + * @param uuid + * @return + */ + public static Long uuidDecodeToTime(String uuid) { + //从指定位置截取数字部分并转换为长整型 + String digits = uuid.substring(2, 4) + uuid.substring(6, 8) + uuid.substring(10, 12) + + uuid.substring(14, 16) + uuid.substring(18, 20); + return Long.valueOf(digits); + } + + /** + * 生成指定长度的随机字符串 + * @param size + * @return + */ + public static String randomUUID(int size) { + Random random = new Random(); + StringBuffer randomStr = new StringBuffer(); + //生成10位的随机字符 + for (int i = 0; i < size; i++) { + int index = random.nextInt(52); + randomStr.append(BASE_CHARS[index]); + } + return randomStr.toString(); + } + + /** + * 将时间戳按照每2位进行分段 + * @param timestamp + * @return + */ + public static List splitTimestamp(long timestamp) { + String timestampStr = String.valueOf(timestamp); + List timestampList = new ArrayList<>(); + for (int i = 0; i < timestampStr.length(); i += 2) { + timestampList.add(timestampStr.substring(i, Math.min(i + 2, timestampStr.length()))); + } + return timestampList; + } + + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/Constant.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/Constant.java new file mode 100644 index 0000000..412c2fa --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/Constant.java @@ -0,0 +1,34 @@ +package com.sonic.cow.utils; + +public class Constant { + + /** + * 缓存的历史消息条数 + */ + public final static Integer MAX_HISTORY_MESSAGE = 100; + + /** + * 缓存的历史消息的过期天数 + */ + public final static Integer TTL_HISTORY_MESSAGE_DAY = 365; + + /** + * 缓存时间(单位:秒) 1 小时 + */ + public final static Integer TTL_CONTEXT_CACHING = 3600; + + /** + * 通用图像提示词 + */ + public final static String COMMON_IMAGE_PROMPT = "professional studio lighting, cinematic lighting, volumetric shadows, medium close-up, soft bokeh background, depth of field, highly detailed, ultra high resolution, 8k, masterpiece, caucasian features, western facial structure, proportional anatomy"; + + /** + * 一条语音预扣金额 10Coin + */ + public final static Long VOICE_DEDUCTION_AMOUNT = 1000L; + + /** + * 语音通话1分钟预扣金额 20Coin + */ + public final static Long VOICE_CALL_DEDUCTION_AMOUNT = 2000L; +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/DateConvert.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/DateConvert.java new file mode 100644 index 0000000..038ebca --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/DateConvert.java @@ -0,0 +1,17 @@ +package com.sonic.cow.utils; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class DateConvert { + + /** + * 将LocalDateTime转换成yyyy-MM-dd + * @param localDateTime + * @return + */ + public static String localDateTime2String(LocalDateTime localDateTime) { + return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/DateUtils.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/DateUtils.java new file mode 100644 index 0000000..f4b9425 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/DateUtils.java @@ -0,0 +1,331 @@ +package com.sonic.cow.utils; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.google.common.base.Strings; +import org.apache.commons.lang3.time.FastDateFormat; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.*; +import java.util.*; + + +/** + * 日期工具类. + * + * @author xi.he + */ +public class DateUtils { + + public static final String Y_M_D_H_M_S = "yyyy-MM-dd HH:mm:ss"; + + public static final String Y_M_D = "yyyy-MM-dd"; + + public static final String YMDHMS = "yyyyMMddHHmmss"; + + public static final String YMD = "yyyyMMdd"; + + public static final String ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + + public static final long ONE_DAY_MillIS = 24 * 60 * 60 * 1000; + + public static final ZoneId ZONE_ID = ZoneId.systemDefault(); + + public static Date convert(Long timestamp) { + return new Date(timestamp); + } + + /** + * 方法描述:获取当前时间的 + */ + public static Date now() { + return new Date(); + } + + + public static String getNow(String pattern) { + return FastDateFormat.getInstance(pattern).format(new Date()); + } + + + public static Long currentTimeMillis() { + return System.currentTimeMillis(); + } + + /** + * 方法描述:格式化日期 + */ + public static String formatDate(Date d, String fmt) { + return FastDateFormat.getInstance(fmt).format(d); + } + + public static String formatY_M_D(Date d) { + return FastDateFormat.getInstance(Y_M_D).format(d); + } + + public static String formatY_M_D_H_M_S(Date d) { + return FastDateFormat.getInstance(Y_M_D_H_M_S).format(d); + } + + public static String formatYMD(Date d) { + return FastDateFormat.getInstance(YMD).format(d); + } + + public static String formatYMDHMS(Date d) { + return FastDateFormat.getInstance(YMDHMS).format(d); + } + + + public static String formatDate(Long date, String fmt) { + return FastDateFormat.getInstance(fmt).format(date); + } + + public static String format(Date date, String fmt) { + return FastDateFormat.getInstance(fmt).format(date); + } + + public static Date parse(String date, String fmt) { + try { + return FastDateFormat.getInstance(fmt).parse(date); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static Date parseY_M_D(String date) { + if (Strings.isNullOrEmpty(date)) { + return null; + } + try { + return FastDateFormat.getInstance(Y_M_D).parse(date); + } catch (ParseException e) { + e.printStackTrace(); + } + return null; + } + + public static Date parseY_M_D_H_M_S(String date) { + try { + return FastDateFormat.getInstance(Y_M_D_H_M_S).parse(date); + } catch (ParseException e) { + throw new IllegalArgumentException("the date pattern is error!"); + } + } + + public static Date parseISO8601(String dateText) { + DateTimeFormatter parser = ISODateTimeFormat.dateTimeNoMillis(); + DateTime dt = parser.parseDateTime(dateText); + return dt.toDate(); + } + + public static Date parseISO8601Mill(String dateText) { + DateTimeFormatter parser = ISODateTimeFormat.dateTime(); + DateTime dt = parser.parseDateTime(dateText); + return dt.toDate(); + } + + + public static Date parseYMD(String date) { + try { + return FastDateFormat.getInstance(YMD).parse(date); + } catch (ParseException e) { + e.printStackTrace(); + } + return null; + } + + public static Date parseYMDHMS(String date) { + try { + return FastDateFormat.getInstance(YMDHMS).parse(date); + } catch (ParseException e) { + throw new IllegalArgumentException("the date pattern is error!"); + } + } + + /** + * 是否是同一天 + */ + public static boolean isSameDay(Date date, Date date2) { + if (date == null || date2 == null) { + return false; + } + FastDateFormat df = FastDateFormat.getInstance(Y_M_D); + return df.format(date).equals(df.format(date2)); + } + + public static Date addMonth(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.MONTH, interval); + return calendar.getTime(); + } + + public static Date addDay(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.DAY_OF_MONTH, interval); + return calendar.getTime(); + } + + public static Date addHour(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.HOUR_OF_DAY, interval); + return calendar.getTime(); + } + + public static Date addMinute(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.MINUTE, interval); + return calendar.getTime(); + } + + public static Date addSecond(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.SECOND, interval); + return calendar.getTime(); + } + + /** + * 获取某一个日期的最小时间 + * @param date + * @return + */ + public static Date getDateMinTime(Date date){ + LocalDateTime endDateTime = LocalDateTime.ofInstant(date.toInstant(),ZONE_ID) + .with(LocalTime.MIN); + return Date.from(endDateTime.atZone(ZONE_ID).toInstant()); + } + /** + * 获取某一个日期的最大时间 + * @param date + * @return + */ + public static Date getDateMaxTime(Date date){ + LocalDateTime endDateTime = LocalDateTime.ofInstant(date.toInstant(),ZONE_ID) + .with(LocalTime.MAX); + return Date.from(endDateTime.atZone(ZONE_ID).toInstant()); + } + + /** + * 获取endDate与startDate日期相隔天数 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return + */ + public static Long differentDays(Date startDate,Date endDate) { + if (startDate == null || endDate == null) { + return 0L; + } + return (endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000); + } + + /** + * 获取endDate与startDate日期之间的日期列表,按pattern格式化 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param pattern 格式化规则 + * @return + */ + public static List differentDaysStr(Date startDate, Date endDate,String pattern) { + List daysList = new ArrayList<>(Arrays.asList(DateUtils.format(startDate,pattern))); + long days = differentDays(startDate,endDate); + for (int i = 1; i < days+1; i++) { + daysList.add(format(addDay(startDate,i),pattern)); + } + return daysList; + } + + public static List getDays(String startTime, String endTime) { + List result = new ArrayList(); + try { + if (StringUtils.isEmpty(startTime) || StringUtils.isEmpty(endTime)) { + return null; + } + //1、定义转换格式 + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + + Date start = df.parse(startTime); + Date end = df.parse(endTime); + + Calendar tempStart = Calendar.getInstance(); + tempStart.setTime(start); + Calendar tempEnd = Calendar.getInstance(); + tempEnd.setTime(end); + + result.add(startTime); + tempStart.add(Calendar.DAY_OF_YEAR, 1); + while (tempStart.before(tempEnd)) { + result.add(df.format(tempStart.getTime())); + tempStart.add(Calendar.DAY_OF_YEAR, 1); + } + if (!startTime.equals(endTime)) { + result.add(endTime); + } + } catch (ParseException e) { + e.printStackTrace(); + } + return result; + } + + + + public static Long getIntervalSeconds(Date start, Integer intervalMin) { + if (start == null) { + return 0L; + } + long intervalSeconds = start.getTime() + intervalMin * 60 * 1000 - System.currentTimeMillis(); + return intervalSeconds > 0L ? intervalSeconds : 0L; + } + + public static void main(String[] args){ + String text = "2020-02-08T15:20:19Z"; + DateTimeFormatter parser = ISODateTimeFormat.dateTimeNoMillis(); + DateTime dt = parser.parseDateTime(text); + DateTimeFormatter formatter = DateTimeFormat.mediumDateTime(); + System.out.println(formatter.print(dt)); + } + + public static String toFormatDate(String strDate) { + try { + Date date = new SimpleDateFormat("yyyyMMdd").parse(strDate); + return new SimpleDateFormat("yyyy-MM-dd").format(date); + } catch (ParseException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 根据年月日字符串计算年龄 + * @param birthDateStr 出生日期字符串,格式为 yyyy-MM-dd + * @return 年龄 + */ + public static int calculateAge(String birthDateStr) { + java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate birthDate = LocalDate.parse(birthDateStr, formatter); + LocalDate currentDate = LocalDate.now(); + return Period.between(birthDate, currentDate).getYears(); + } + + + /** + * 获取两个时间相差的秒数 + * @param startTime + * @param endTime + * @return + */ + public static Long getTimeDifference(LocalDateTime startTime, LocalDateTime endTime) { + Duration duration = Duration.between(startTime, endTime); + return duration.getSeconds(); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/HttpUtil.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/HttpUtil.java new file mode 100644 index 0000000..dcdd440 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/HttpUtil.java @@ -0,0 +1,76 @@ +package com.sonic.cow.utils; + +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +/** + * @description: Http + * @author: mzc + * @date: 2023-02-02 15:06 + **/ +public class HttpUtil { + + /** + * 获取body内容 + * + * @param request + * @return + * @throws Exception + */ + public static String getBody(HttpServletRequest request) throws Exception { + String body; + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader bufferedReader = null; + try { + InputStream inputStream = request.getInputStream(); + if (inputStream != null) { + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + char[] charBuffer = new char[128]; + int bytesRead = -1; + while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { + stringBuilder.append(charBuffer, 0, bytesRead); + } + } else { + stringBuilder.append(""); + } + } catch (IOException ex) { + throw ex; + } finally { + if (bufferedReader != null) { + try { + bufferedReader.close(); + } catch (IOException ex) { + throw ex; + } + } + } + body = stringBuilder.toString(); + return body; + } + + + /** + * 获取请求头信息 + * + * @param request + * @return + */ + public static Map getHeadersInfo(HttpServletRequest request) { + Map map = new HashMap(); + @SuppressWarnings("rawtypes") + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String key = (String) headerNames.nextElement(); + String value = request.getHeader(key); + map.put(key, value); + } + return map; + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/JsonExtractor.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/JsonExtractor.java new file mode 100644 index 0000000..294f05e --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/JsonExtractor.java @@ -0,0 +1,62 @@ +package com.sonic.cow.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sonic.cow.domain.bo.ChatOutputTextBo; +import lombok.extern.slf4j.Slf4j; + +import java.math.BigDecimal; + +@Slf4j +public class JsonExtractor { + + /** + * 提取 JSON 字符串中的 message 值,并封装到 ChatOutputTextBo 对象中。 + * + * @param input 输入的字符串,包含 JSON 和可能的非 JSON 内容 + * @return ChatOutputTextBo 对象,包含 message 和 score,如果解析失败则返回默认对象 + */ + public static ChatOutputTextBo extractMessageAndScore(String input) { + try { + ObjectMapper mapper = new ObjectMapper(); + // 清理输入,提取可能的 JSON 部分(假设 JSON 以 { 开头,以 } 结尾) + String jsonString = extractJsonString(input); + if (jsonString != null) { + JsonNode node = mapper.readTree(jsonString); + if (node.has("message") && node.has("score")) { + return ChatOutputTextBo.builder() + .message(node.get("message").asText()) + .score(BigDecimal.valueOf(node.get("score").asDouble())) + .build(); + } + } + } catch (Exception e) { + log.error("Failed to parse JSON: ", e); + } + // 默认返回值 + return ChatOutputTextBo.builder() + .message(input) + .score(new BigDecimal("0")) + .build(); + } + + /** + * 从输入字符串中提取有效的 JSON 部分。 + * + * @param input 输入字符串 + * @return 提取的 JSON 字符串,如果无法提取则返回 null + */ + private static String extractJsonString(String input) { + if (input == null || input.trim().isEmpty()) { + return null; + } + // 找到 JSON 对象的起始和结束位置 + int startIndex = input.indexOf("{"); + int endIndex = input.lastIndexOf("}"); + if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) { + return input.substring(startIndex, endIndex + 1).replace("\\n", ""); + } + return null; + } + +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/KeyGenerator.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/KeyGenerator.java new file mode 100644 index 0000000..2ade77e --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/KeyGenerator.java @@ -0,0 +1,29 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.sonic.cow.utils; + +import java.util.UUID; + +public class KeyGenerator extends AbstractKeyGenerator { + private static final KeyGenerator SINGLE = new KeyGenerator(); + + private KeyGenerator() { + } + + public static KeyGenerator instance() { + return SINGLE; + } + + public static String UUID() { + return UUID.randomUUID().toString(); + } + + @Override + public String customKey() { + return ""; + } + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/LimitUtils.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/LimitUtils.java new file mode 100644 index 0000000..46eafb2 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/LimitUtils.java @@ -0,0 +1,124 @@ +package com.sonic.cow.utils; + +import com.sonic.common.AppRuntime; +import com.sonic.cow.domain.bo.LimitBo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 限流工具类 + */ +@Slf4j +@Component +public class LimitUtils { + + @Autowired + private AppRuntime appRuntime; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * redis key不存在的过期时间值 + */ + private Integer NULL_KEY_EXP_TIME_VALUE = -2; + + /** + * redis key永不过期的过期时间值 + */ + private Integer NEVER_EXPIRES_VALUE = -1; + + /** + * 根据请求参数进行限流检查 + * @param redisKey 自定义封装的redisKey + * @param count 限流的数量 + * @param time 时间段:单位为秒 + */ + public boolean defaultLimitCheckByKey(String redisKey, int count, int time) { + //用户操作某个话题的点赞,超过4次就提示,锁定5s + int num; + Long expTime = stringRedisTemplate.getExpire(redisKey); + if (expTime == null || expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", time, TimeUnit.SECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(redisKey, 1).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis并返回false + stringRedisTemplate.delete(redisKey); + return false; + } + if (num > count) { + log.info("===>超过了限定的次数[" + count + "]"); + return true; + } + return false; + } + + /** + * 根据请求参数进行限流检查 + * @param redisKey 自定义封装的redisKey + * @param count 限流的数量 + * @param time 时间段:单位为秒 + */ + public LimitBo defaultLimitCheckByKeyV2(String redisKey, int count, int time) { + LimitBo bo = new LimitBo(); + //用户操作某个话题的点赞,超过4次就提示,锁定5s + int num; + Long expTime = stringRedisTemplate.getExpire(redisKey); + bo.setExpTime(expTime); + if (expTime == null || expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", time, TimeUnit.SECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(redisKey, 1).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis并返回false + stringRedisTemplate.delete(redisKey); + bo.setLimitBl(false); + return bo; + } + if (num > count) { + log.info("===>超过了限定的次数[" + count + "]"); + bo.setLimitBl(true); + return bo; + } + bo.setLimitBl(false); + return bo; + } + + + /** + * 根据请求参数进行限流检查 + * @param redisKey 自定义封装的redisKey + * @param time 时间段:单位为秒 + */ + public int defaultLimitCheckReturnCount(String redisKey, int time) { + //用户操作某个话题的点赞,超过4次就提示,锁定5s + int num; + Long expTime = stringRedisTemplate.getExpire(redisKey); + if (expTime == null || expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", time, TimeUnit.SECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(redisKey, 1).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis并返回false + stringRedisTemplate.delete(redisKey); + return 0; + } + return num; + } + + +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/MD5Util.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/MD5Util.java new file mode 100644 index 0000000..981af44 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/MD5Util.java @@ -0,0 +1,67 @@ +package com.sonic.cow.utils; + +import org.apache.commons.codec.digest.DigestUtils; + +import java.io.UnsupportedEncodingException; +import java.security.SignatureException; + +/** + * md5加密 + * + * @author Xi.He + * + */ +public class MD5Util { + + /** + * 加密字符串(大写) + * @param text 需要加密的字符串 + * @param charset 编码格式 + * @return 签名结果 + */ + public static String upperDigest(String text, String charset) { + return DigestUtils.md5Hex(getContentBytes(text, charset)).toUpperCase(); + } + + /** + * 加密字符串(小写) + * @param text 需要加密的字符串 + * @param charset 编码格式 + * @return 签名结果 + */ + public static String digest(String text, String charset) { + return DigestUtils.md5Hex(getContentBytes(text, charset)); + } + + + /** + * 加密字符串(小写) + * @param text 需要加密的字符串 + * @return 签名结果 + */ + public static String digest(String text) { + return DigestUtils.md5Hex(getContentBytes(text, "UTF-8")); + } + + + /** + * @param content 内容 + * @param charset 编码 + * @return byte[] + * @throws SignatureException + * @throws UnsupportedEncodingException + */ + private static byte[] getContentBytes(String content, String charset) { + if (charset == null || "".equals(charset)) { + return content.getBytes(); + } + try { + return content.getBytes(charset); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + public static void main(String[] args) { + System.out.println(digest(digest("Danong2018"))); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/RedisKeyUtils.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/RedisKeyUtils.java new file mode 100644 index 0000000..6cc15ed --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/RedisKeyUtils.java @@ -0,0 +1,319 @@ +package com.sonic.cow.utils; + +import com.sonic.common.AppRuntime; +import com.sonic.cow.enums.PromptTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.validation.constraints.NotNull; + +/** + * 统一的 redis key 管理工具类(方便后续的维护和查找) + */ +@Slf4j +@Service +public class RedisKeyUtils { + + @Autowired + private AppRuntime appRuntime; + + /** + * 和AI聊天时的上下文缓存ID(eg: 1010:chat:contextId:xx:xx) + * + * @param userId + * @param aiId + * @return + */ + public String chatContextIdCacheKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("chat", "contextId", userId, aiId); + } + + /** + * 和AI聊天时的上下文缓存ID(eg: 1010:chat:responseId:xx:xx) + * + * @param userId + * @param aiId + * @return + */ + public String chatResponseIdCacheKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("chat", "responseId", userId, aiId); + } + + /** + * 和AI聊天时的历史消息缓存(eg: 1010:chat:contextId:xx:xx) + * + * @param userId + * @param aiId + * @return + */ + public String chatMemoryCacheKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("chat", "memory", userId, aiId); + } + + /** + * 生成图片用户ID限流 + * + * @param userId + * @param hl + * @return + */ + public String genImageUserLimit(Long userId, Boolean hl) { + return appRuntime.buildPrefixKey("limit", "genImage", "userId", userId, hl); + } + + /** + * 生成图片设备号限流 + * + * @param deviceId + * @param hl + * @return + */ + public String genImageDeviceLimit(String deviceId, Boolean hl) { + return appRuntime.buildPrefixKey("limit", "genImage", "deviceId", deviceId, hl); + } + + /** + * 生成图片IP限流 + * + * @param ip + * @param hl + * @return + */ + public String genImageIpLimit(String ip, Boolean hl) { + return appRuntime.buildPrefixKey("limit", "genImage", "ip", ip, hl); + } + + /** + * 轮询获取图片生成结果限流 + * + * @param batchNo + * @return + */ + public String genImagePollingLimit(String batchNo) { + return appRuntime.buildPrefixKey("limit", "genImage", "polling", batchNo); + } + + /** + * 轮询获取图片生成结果限流 + * + * @param batchNo + * @return + */ + public String getGenImageCacheKey(String batchNo) { + return appRuntime.buildPrefixKey("cache", "getGenImage", batchNo); + } + + /** + * 生成图片的锁 + * + * @param userId + * @return + */ + public String genImageLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "genImage", userId); + } + + /** + * AI用户基础信息key 在sonic_cow服务中使用,构建聊天提示词,图片生成提示词等 + * + * @param aiId + * @return + */ + public String aiUserCacheInfoKey(Long aiId) { + return "aiUserCacheInfo" + aiId; + } + + + /** + * 生成AI用户内容限流 + * + * @param userId + * @param bizType + * @return + */ + public String limitGenAiUserContentKey(Long userId, String bizType) { + return appRuntime.buildPrefixKey("limit", "genAiUserContent", userId, bizType); + } + + /** + * ai图生图加锁 + * + * @return + */ + public String aiGenImageLockKey(String batchNo) { + return appRuntime.buildPrefixKey("aiGenImageBatchNoLock", batchNo); + } + + /** + * ai图生图加锁 + * + * @return + */ + public String aiChatLockKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("aiChatLockKey", userId, aiId); + } + + /** + * 获取用户通知的zset(超过24小时没有和用户联系时触发扫描通知) + * + * @return + */ + public String notifyUserZSetKey() { + return appRuntime.buildPrefixKey("chat", "notify"); + } + + /** + * 图生图错误枚举 + */ + public String imageGenImageErrorEnumKey(String imageUrl) { + return appRuntime.buildPrefixKey("imageGenImageErrorEnum"); + } + + /** + * 和AI聊天时的上下文缓存清理任务(eg: 1010:chat:responseId:clear:xx:xx) + * + * @param userId + * @param aiId + * @return + */ + public String chatResponseIdClearTaskKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("chat", "responseId", "clear", userId, aiId); + + } + + /** + * 语音通话聊天缓存key + * + * @param userId + * @param aiId + * @return + */ + public String voiceChatMessageCacheKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("voiceChatMessageCache", userId, aiId); + + } + + /** + * 语音通话用户离开房间缓存key + * + * @return + */ + public String voiceChatUserLeaveRoomKey() { + return appRuntime.buildPrefixKey("voiceChatUserLeaveRoom"); + } + + /** + * 语音通话用户ID与任务ID缓存key + * + * @param userId + * @return + */ + public String voiceChatTaskIdKey(String userId) { + return appRuntime.buildPrefixKey("voiceChatTaskId", userId); + } + + /** + * 语音通话说话轮次缓存key + * + * @param taskId + */ + public String voiceChatRoundIdKey(String taskId) { + return appRuntime.buildPrefixKey("voiceChatRoundId", taskId); + } + + /** + * 语音通话说话每轮数据缓存key + * + * @param taskId + */ + public String voiceChatRoundDataKey(String taskId) { + return appRuntime.buildPrefixKey("voiceChatRoundData", taskId); + } + + /** + * 语音通话开始时间缓存key + * + * @param roomId + * @param taskId + * @return + */ + public String voiceChatStartTimeCacheKey(String roomId, String taskId) { + return appRuntime.buildPrefixKey("voiceChatStartTime", roomId, taskId); + } + + /** + * 语音通话ID缓存key + * + * @param roomId + * @param taskId + * @return + */ + public String voiceCallIdCacheKey(String roomId, String taskId) { + return appRuntime.buildPrefixKey("voiceCallIdCache", roomId, taskId); + } + + + /** + * 扣费限流 + * + * @param userId + * @return + */ + public String ttsDeAmountLimitKey(Long userId) { + return appRuntime.buildPrefixKey("limit", "ttsDeAmount", userId); + } + + /** + * 生成辅助聊天内容限流 + * + * @param userId + * @param batchNo + * @return + */ + public String genSupContentBatchNoLimitKey(Long userId, String batchNo) { + return appRuntime.buildPrefixKey("limit", userId, batchNo); + } + + /** + * 生成自动聊天内容的限流key + * + * @param userId + * @param aiId + * @return + */ + public String genAutoChatAndSendImMessageLimitKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("limit", userId, aiId); + } + + /** + * tts调用限制键 1分钟最多调20次 + * + * @param userId + * @return + */ + public String ttsMinuteLimitKey(Long userId) { + return appRuntime.buildPrefixKey("limit", "ttsMinute", userId); + } + + /** + * 内容生成限流key 1分钟最多调20次 + * + * @param userId + * @param ptType + * @return + */ + public String contentLimitKey(Long userId, PromptTypeEnum ptType) { + return appRuntime.buildPrefixKey("limit", "content", userId, ptType); + } + + /** + * 创建Ai形象时限流key 1分钟最多调20次 + * + * @param userId + * @return + */ + public String aiImageLimitKey(Long userId) { + return appRuntime.buildPrefixKey("limit", "AiImage", userId); + } +} diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/ResponseFormatJSONSchemaJSONSchemaParam.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/ResponseFormatJSONSchemaJSONSchemaParam.java new file mode 100644 index 0000000..da33255 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/ResponseFormatJSONSchemaJSONSchemaParam.java @@ -0,0 +1,66 @@ +package com.sonic.cow.utils; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.JsonNode; + + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResponseFormatJSONSchemaJSONSchemaParam { + String name; + String description; + JsonNode schema; + boolean strict; + + @Override + public String toString() { + return "ResponseFormatJSONSchemaJSONSchemaParam{" + + "name='" + name + '\'' + + ", description='" + description + '\'' + + ", schema=" + schema + + ", strict=" + strict + + '}'; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public JsonNode getSchema() { + return schema; + } + + public void setSchema(JsonNode schema) { + this.schema = schema; + } + + public boolean isStrict() { + return strict; + } + + public void setStrict(boolean strict) { + this.strict = strict; + } + + + public ResponseFormatJSONSchemaJSONSchemaParam(String name) { + this.name = name; + } + + public ResponseFormatJSONSchemaJSONSchemaParam(String name, String description, JsonNode schema, boolean strict) { + this.name = name; + this.description = description; + this.schema = schema; + this.strict = strict; + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/TextCleaner.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/TextCleaner.java new file mode 100644 index 0000000..386bece --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/TextCleaner.java @@ -0,0 +1,37 @@ +package com.sonic.cow.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +@Slf4j +public class TextCleaner { + /** + * Removes all occurrences of tags and their enclosed content from the input text. + * + * @param text The input text to process. + * @return The text with the specified tags and their content removed. + */ + public static String removeAiCharacterPrompt(String text) { + try { + if (text == null) { + return null; + } + // Compile pattern with DOTALL to handle multi-line content + Pattern pattern = Pattern.compile(".*?", Pattern.DOTALL); + Matcher matcher = pattern.matcher(text); + return matcher.replaceAll(""); + } catch (Exception e) { + log.error("removeAiCharacterPrompt error:", e); + } + return text; + } + + public static void main(String[] args) { + // Example usage + String input = "要为这个 AI 虚拟角色写出精准、生动的简介,我需要了解角色的具体设定。请在 之间提供角色的背景、性格、能力、外观或其他关键信息,我会据此为您生成不超过 200 字的角色简介。提示词不接受任意输入的一串内容作为输入,这种情况下可能还是需要扩写才行吧"; + String output = removeAiCharacterPrompt(input); + System.out.println(output); + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/java/com/sonic/cow/utils/TimerUtils.java b/sonic-cow/server/src/main/java/com/sonic/cow/utils/TimerUtils.java new file mode 100644 index 0000000..f555a17 --- /dev/null +++ b/sonic-cow/server/src/main/java/com/sonic/cow/utils/TimerUtils.java @@ -0,0 +1,32 @@ +package com.sonic.cow.utils; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * @description: 代码执行时长工具类 + * @author: mzc + * @date: 2025-10-21 10:12 + **/ +@Slf4j +public class TimerUtils { + // 开始计时,返回当前时间戳(纳秒) + public static long start() { + return System.nanoTime(); + } + + // 结束计时并打印时长(默认毫秒) + public static void logCost(long startTime, String stepName) { + long costNanos = System.nanoTime() - startTime; + long costMs = TimeUnit.NANOSECONDS.toMillis(costNanos); + log.info("{} 执行时长:{} ms", stepName, costMs); + } + + // 重载:支持自定义时间单位(如微秒、秒) + public static void logCost(long startTime, String stepName, TimeUnit unit) { + long cost = unit.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS); + System.out.printf("[%s] 执行时长:%d %s%n", stepName, cost, unit.name().toLowerCase()); + log.info("{} 执行时长:{} {}", stepName, cost, unit.name().toLowerCase()); + + } +} \ No newline at end of file diff --git a/sonic-cow/server/src/main/resources/application-dev.yml b/sonic-cow/server/src/main/resources/application-dev.yml new file mode 100644 index 0000000..34cf6cf --- /dev/null +++ b/sonic-cow/server/src/main/resources/application-dev.yml @@ -0,0 +1,100 @@ +spring: + datasource: + # TODO: 需要改写为Dev环境的Mysql地址, 数据库名称,用户名和密码 + url: jdbc:mysql://54.223.196.180:3306/sonic-cow?useSSL=false&characterEncoding=utf-8&autoReconnect=true + username: root + password: toukagames1234 + + redis: + # TODO: 需要改写为DEV环境的Redis地址, 数据库名称和密码 + host: 54.223.196.180 + port: 6379 + database: 0 + password: 123456 + # cluster: + # nodes: 192.168.100.238 + # ssl: false + + rabbitmq: + # TODO: 需要改写为dev环境的rabbitmq地址, 用户名和密码 + host: 54.223.196.180 + port: 5672 + username: guest + password: toukagames1234 + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bs.controller" + +# 火山引擎 +volcengine: + # 火山引擎的api地址 + baseUrl: "https://ark.ap-southeast.bytepluses.com" + # 火山引擎的apiKey + apiKey: 3eb4bcb7-e388-463e-87ee-d13299ffc24c + # 上下文缓存模型 + contextCachingModel: ep-20250709162415-thcvd + # DeepSeekV3.1模型 + deepSeekModel: ep-20250910114251-szxqk + # 文生图模型 + textToImageModel: ep-20250808171219-llgmv + # 文生图,图生图 + seedream4Model: ep-20250911184318-89dxw + # 国内 访问访问key或密钥 图生图使用 + cnAccessKey: AKLTOGYyN2MwYmRiY2QwNDkyMmI5MmMxYTA2MGU4NjBjNTE + cnSecretKey: T1Rjek9EQXlNRGRqTWpWa05HWTNPR0ZrT0dFNU56ZG1aVGRsWldOa05UYw== + # 国外 访问访问key或密钥 + AccessKey: AKAPMDJkZWFiMDRiNWI3NDhjNjhjY2VkNThmOTFlMmU0NDI + SecretKey: WkRSaU1EVmpZemhpT1dWbE5EWXdaR0ZsT0RJNU1XTTBZbVEzTUdSa016WQ== + #RTC 语音通话 appId appSecret配置 + region: ap-singapore-1 + rtc: + service: rtc + appId: 6901d3b17baf870173b6e26c + appKey: 42c759691d5d4d1f98c61bfa0d337cc0 + host: open.byteplusapi.com + url: https://open.byteplusapi.com/?Action=StartVoiceChat&Version=2025-05-01 + action: + startVoiceChat: StartVoiceChat + stopVoiceChat: StopVoiceChat + updateVoiceChat: UpdateVoiceChat + version: 2025-05-01 + webhookSecret: AMTm7g05KcxXtoTR + subtitleServiceUrl: https://test-cow.crushlevel.ai/web/voice-chat/rts-callback + subtitleServiceSignature: 456321 + asr: + appId: 2145873908 + accessToken: 7aGF1WyUWC6SwJTdHnrb9YZD604rJmFK + provider: BytePlus + resourceId: volc.bigasr.auc_turbo + flashUrl: https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash + #国内环境语音识别 + asr-cn: + appId: 2881520229 + accessToken: DDrnO8OJufJmNaH2V-QE9zwxVACR0rnD + resourceId: volc.bigasr.auc_turbo + flashUrl: https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash + + tts: + appId: 2145873908 + accessToken: 7aGF1WyUWC6SwJTdHnrb9YZD604rJmFK + provider: BytePlus + cluster: volcano_icl + ttsUrl: https://openspeech.bytedance.com/api/v1/tts + tts-cn: + appId: 2881520229 + accessToken: DDrnO8OJufJmNaH2V-QE9zwxVACR0rnD + provider: BytePlus + cluster: volcano_icl + ttsUrl: https://openspeech.bytedance.com/api/v1/tts + + llm: + endPointId: ep-20250821145110-mfx7k + apiKey: 4075846b-cb9e-45f6-bf59-ab0a94745d9b \ No newline at end of file diff --git a/sonic-cow/server/src/main/resources/application-local.yml b/sonic-cow/server/src/main/resources/application-local.yml new file mode 100644 index 0000000..9d48322 --- /dev/null +++ b/sonic-cow/server/src/main/resources/application-local.yml @@ -0,0 +1,93 @@ +spring: + datasource: + # TODO: 需要改写为Dev环境的Mysql地址, 数据库名称,用户名和密码 + url: jdbc:mysql://192.168.100.238:3306/sonic-cow?useSSL=false&characterEncoding=utf-8&autoReconnect=true + username: egirl_dev + password: lpkq609oI9eRc + + redis: + # TODO: 需要改写为DEV环境的Redis地址, 数据库名称和密码 + host: 192.168.100.238 + port: 6379 + database: 0 + password: Epal@2020 + # cluster: + # nodes: 192.168.100.238 + # ssl: false + + rabbitmq: + # TODO: 需要改写为dev环境的rabbitmq地址, 用户名和密码 + host: 192.168.100.238 + port: 5672 + username: guest + password: epal@2020 + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bs.controller" + +# 火山引擎 +volcengine: + # 火山引擎的api地址 + baseUrl: "https://ark.ap-southeast.bytepluses.com" + # 火山引擎的apiKey + apiKey: 4075846b-cb9e-45f6-bf59-ab0a94745d9b + # 上下文缓存模型 + contextCachingModel: ep-20250709162415-thcvd + # 文生图模型 + textToImageModel: ep-20250808171219-llgmv + # 国内 访问访问key或密钥 图生图使用 + cnAccessKey: AKLTOGYyN2MwYmRiY2QwNDkyMmI5MmMxYTA2MGU4NjBjNTE + cnSecretKey: T1Rjek9EQXlNRGRqTWpWa05HWTNPR0ZrT0dFNU56ZG1aVGRsWldOa05UYw== + # 国外 访问访问key或密钥 + accessKey: AKAPNjdlODVmYzJjNGU4NGU5Njg0M2FhNWRiM2U2ZjY0YTI + secretKey: TkdVMFpHUTJNall4TkRJNU5HUTRZMkZqT1dVNVpHSXdaR1ptT1RjNFl6aw== + #RTC 语音通话 appId appSecret配置 + region: ap-singapore-1 + rtc: + service: rtc + appId: 689ade491323ae01797818e0 + appKey: f27ea44dfa8c49ba9c420963e726ccbb + host: open.byteplusapi.com + url: https://${volcengine.rtc.host}?Action=%s&Version=%s + action: + startVoiceChat: StartVoiceChat + stopVoiceChat: StopVoiceChat + updateVoiceChat: UpdateVoiceChat + version: 2025-05-01 + webhookSecret: AMTm7g05KcxXtoTR + asr: + appId: 7330233595 + accessToken: NxFxll0gDGl3W0TWW1Fqh23PzKaaQiLu + provider: BytePlus + resourceId: volc.bigasr.auc_turbo + flashUrl: https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash + #国内环境语音识别 + asr-cn: + appId: 2881520229 + accessToken: DDrnO8OJufJmNaH2V-QE9zwxVACR0rnD + resourceId: volc.bigasr.auc_turbo + flashUrl: https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash + + tts: + appId: 7330233595 + accessToken: NxFxll0gDGl3W0TWW1Fqh23PzKaaQiLu + provider: BytePlus + cluster: volcano_icl + ttsUrl: https://openspeech.bytedance.com/api/v1/tts + tts-cn: + appId: 2881520229 + accessToken: DDrnO8OJufJmNaH2V-QE9zwxVACR0rnD + provider: BytePlus + cluster: volcano_icl + ttsUrl: https://openspeech.bytedance.com/api/v1/tts + + llm: + endPointId: ep-20250709162415-thcvd \ No newline at end of file diff --git a/sonic-cow/server/src/main/resources/application-prod.yml b/sonic-cow/server/src/main/resources/application-prod.yml new file mode 100644 index 0000000..fd64f5b --- /dev/null +++ b/sonic-cow/server/src/main/resources/application-prod.yml @@ -0,0 +1,85 @@ +spring: + datasource: + url: ${DB.MASTER.COW.URL} + username: ${DB.MASTER.USERNAME} + password: ${DB.MASTER.PASSWORD} + + redis: +# host: ${REDIS.MAIN.HOST} +# port: ${REDIS.MAIN.PORT} +# database: 0 +# password: ${REDIS.MAIN.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + + rabbitmq: + # TODO: 需要改写为测试环境的rabbitmq地址, 用户名和密码 + host: ${MQ.HOST} + port: ${MQ.PORT} + username: ${MQ.USERNAME} + password: ${MQ.PASSWORD} + ssl: + enabled: true + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bs.controller" + +# 火山引擎 +volcengine: + # 火山引擎的api地址 + baseUrl: "https://ark.cn-beijing.volces.com" + # 火山引擎的apiKey + apiKey: b887034e-acfd-4a1b-942b-eab2a928b490 + # 上下文缓存模型 + contextCachingModel: ep-20250721115730-twhj2 + # 文生图模型 + textToImageModel: ep-20250718142126-wlb2c + # 国内 访问访问key或密钥 图生图使用 + cnAccessKey: AKLTOGYyN2MwYmRiY2QwNDkyMmI5MmMxYTA2MGU4NjBjNTE + cnSecretKey: T1Rjek9EQXlNRGRqTWpWa05HWTNPR0ZrT0dFNU56ZG1aVGRsWldOa05UYw== + # 国外 访问访问key或密钥 + accessKey: AKAPNjdlODVmYzJjNGU4NGU5Njg0M2FhNWRiM2U2ZjY0YTI + secretKey: TkdVMFpHUTJNall4TkRJNU5HUTRZMkZqT1dVNVpHSXdaR1ptT1RjNFl6aw== + # TODO RTC 语音通话 appId appSecret配置 + region: cn-beijing + rtc: + service: rtc + appId: 687a11a2837ff80175f0138e + appKey: xxxxxKey + # TODO 国内,线上需要替换成open-ap-singapore-1.volcengineapi.com + host: rtc.volcengineapi.com + url: https://${volcengine.rtc.host}?Action=%s&Version=%s + action: + startVoiceChat: StartVoiceChat + stopVoiceChat: StopVoiceChat + updateVoiceChat: UpdateVoiceChat + version: 2024-12-01 + #TODO 回调处理 + webhookSecret: AMTm7g05KcxXtoTR + #TODO 线上需要替换 + subtitleServiceUrl: https://test-cow.crushlevel.ai/web/voice-chat/rts-callback + subtitleServiceSignature: 456321 + #TODO 语音识别 待替换 + asr: + appId: 9546709439 + accessToken: XlP5fxRogd0iHG3tWb6kKZASUIyMXtdQ + resourceId: volc.bigasr.auc_turbo + flashUrl: https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash + #TODO 语音合成 待替换 + tts: + appId: 9546709439 + accessToken: XlP5fxRogd0iHG3tWb6kKZASUIyMXtdQ + provider: volcano + cluster: volcano_icl + ttsUrl: https://openspeech.bytedance.com/api/v1/tts + #TODO 大语言模型 待替换 + llm: + endPointId: ep-20250721115730-twhj2 \ No newline at end of file diff --git a/sonic-cow/server/src/main/resources/application-test.yml b/sonic-cow/server/src/main/resources/application-test.yml new file mode 100644 index 0000000..2b1678e --- /dev/null +++ b/sonic-cow/server/src/main/resources/application-test.yml @@ -0,0 +1,101 @@ +spring: + datasource: + url: ${DB.MASTER.COW.URL} + username: ${DB.MASTER.USERNAME} + password: ${DB.MASTER.PASSWORD} + + redis: +# host: ${REDIS.MAIN.HOST} +# port: ${REDIS.MAIN.PORT} +# database: 0 +# password: ${REDIS.MAIN.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + + rabbitmq: + # TODO: 需要改写为测试环境的rabbitmq地址, 用户名和密码 + host: ${MQ.HOST} + port: ${MQ.PORT} + username: ${MQ.USERNAME} + password: ${MQ.PASSWORD} + ssl: + enabled: true + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bs.controller" + +# 火山引擎 +volcengine: + # 火山引擎的api地址 + baseUrl: "https://ark.ap-southeast.bytepluses.com" + # 火山引擎的apiKey + apiKey: 4075846b-cb9e-45f6-bf59-ab0a94745d9b + # 上下文缓存模型 + contextCachingModel: ep-20250924173624-7r6kb + # response Url + responseUrl: https://ark.ap-southeast.bytepluses.com/api/v3/responses + # DeepSeekV3.1模型 + deepSeekModel: ep-20250910114251-szxqk + # 文生图模型 + textToImageModel: ep-20250808171219-llgmv + # 文生图,图生图 + seedream4Model: ep-20250911184318-89dxw + # 国内 访问访问key或密钥 图生图使用 + cnAccessKey: AKLTOGYyN2MwYmRiY2QwNDkyMmI5MmMxYTA2MGU4NjBjNTE + cnSecretKey: T1Rjek9EQXlNRGRqTWpWa05HWTNPR0ZrT0dFNU56ZG1aVGRsWldOa05UYw== + # 国外 访问访问key或密钥 + accessKey: AKAPNjdlODVmYzJjNGU4NGU5Njg0M2FhNWRiM2U2ZjY0YTI + secretKey: TkdVMFpHUTJNall4TkRJNU5HUTRZMkZqT1dVNVpHSXdaR1ptT1RjNFl6aw== + #RTC 语音通话 appId appSecret配置 + region: ap-singapore-1 + rtc: + service: rtc + appId: 689ade491323ae01797818e0 + appKey: f27ea44dfa8c49ba9c420963e726ccbb + host: open.byteplusapi.com + url: https://open.byteplusapi.com/?Action=%s&Version=%s + action: + startVoiceChat: StartVoiceChat + stopVoiceChat: StopVoiceChat + updateVoiceChat: UpdateVoiceChat + version: 2025-05-01 + webhookSecret: AMTm7g05KcxXtoTR + subtitleServiceUrl: https://test-cow.crushlevel.ai/web/voice-chat/rts-callback + subtitleServiceSignature: 456321 + asr: + appId: 7330233595 + accessToken: NxFxll0gDGl3W0TWW1Fqh23PzKaaQiLu + provider: BytePlus + resourceId: volc.bigasr.auc_turbo + flashUrl: https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash + #国内环境语音识别 + asr-cn: + appId: 2881520229 + accessToken: DDrnO8OJufJmNaH2V-QE9zwxVACR0rnD + resourceId: volc.bigasr.auc_turbo + flashUrl: https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash + + tts: + appId: 7330233595 + accessToken: NxFxll0gDGl3W0TWW1Fqh23PzKaaQiLu + provider: BytePlus + cluster: volcano_icl + ttsUrl: https://openspeech.bytedance.com/api/v1/tts + tts-cn: + appId: 2881520229 + accessToken: DDrnO8OJufJmNaH2V-QE9zwxVACR0rnD + provider: BytePlus + cluster: volcano_icl + ttsUrl: https://openspeech.bytedance.com/api/v1/tts + + llm: + endPointId: ep-20250924173624-7r6kb + apiKey: 4075846b-cb9e-45f6-bf59-ab0a94745d9b diff --git a/sonic-cow/server/src/main/resources/application.yml b/sonic-cow/server/src/main/resources/application.yml new file mode 100644 index 0000000..c2b21f2 --- /dev/null +++ b/sonic-cow/server/src/main/resources/application.yml @@ -0,0 +1,110 @@ +spring: + profiles: + # profile目前支持以下5种:local/unittest/dev/test/product + # 开发的时候一般使用dev或者local + # 在测试环境/生产环境,该配置不起作用,会被外部传入的jvm启动参数(spring.profiles.active)或者环境变量覆盖 + active: dev + application: + # TODO: 更换项目名称 + name: cow + # 必须使用引号,否则会转成8进制 + id: 1010 + task: + execution: + pool: + max-size: 50 + core-size: 4 + queue-capacity: 20480 + keep-alive: 30s + # TODO: 如果不需要mysql,请移除datasource相关的所有配置 + datasource: + driver-class-name: com.mysql.jdbc.Driver + hikari: + auto-commit: true + connection-timeout: 20000 + maximum-pool-size: 30 + minimum-idle: 5 + idle-timeout: 300000 + max-lifetime: 1200000 + # TODO: 如果不需要Redis,请移除redis相关的所有配置 + redis: + lettuce: + pool: + max-active: 1000 + max-wait: 1000 + max-idle: 100 + + rabbitmq: + listener: + simple: + acknowledge-mode: manual + concurrency: 2 + max-concurrency: 10 + #限流 + prefetch: 1 + +# TODO: 如果不需要mysql,请移除mybatis-plus相关的所有配置 +mybatis-plus: + # 定义mybatis映射文件的位置 + mapper-locations: classpath:/mapper/*Mapper.xml + +web: + frequency-alert: + # TODO 修改接口访问频率告警配置, 如果不配置的FrequencyAlertInterceptor不会生效. + # 以下配置含义为: /* 1秒访问20次, /index 10秒访问100次. 访问频率超过该规则会触发告警consumer + rules: + - /*:1/20 + - /index:10/100 + +mq: + # 如无必要,无须修改 exchange + exchange: message-server-exchange + default: + # TODO: {Event.BuildInScene.code}-{appName}-queue + queue: bs-demo-queue + # TODO: {Event.BuildInScene.code}-{appName}-routing-key + routing-key: bs-demo-routing-key + # ai生成图片-文生图 + ai-text-gen-image: + queue: ai-text-gen-image-queue + routing-key: ai-text-gen-image-routing-key + # ai生成图片-图生图 + ai-image-gen-image: + queue: ai-image-gen-image-queue + routing-key: ai-image-gen-image-routing-key + #AI聊天的队列 + aiChat: + queue: ai-chat-queue + routing-key: ai-chat-routing-key + # 心动等级计算队列 + calc-heartbeat-level: + queue: calc-heartbeat-level-queue + routing-key: calc-heartbeat-level-routing-key + # 情绪感知打分 + emotion-score: + queue: emotion-score-queue + routing-key: emotion-score-routing-key + # 文本,语音,语音通话预扣款统计 + user-deduction-stat: + queue: user-deduction-stat-queue + routing-key: user-deduction-stat-routing-key + # 语音通话预扣款死信队列,先发送到死信队列,1分钟转发到预扣款队列 + voice-call-deduction-dead: + queue: voice-call-deduction-dead-queue + routing-key: voice-call-deduction-dead-routing-key + # 语音通话预扣款队列,每分钟发起预扣除费用 + voice-call-deduction: + queue: voice-call-deduction-queue + routing-key: voice-call-deduction-routing-key + # 语音通话回调处理 + voice-call-webhook: + queue: voice-call-webhook-queue + routing-key: voice-call-webhook-routing-key + #文本,语音,语音通话预扣款,如果余额不足,发起扣款 + user-balance-insufficient-checkout: + queue: user-balance-insufficient-checkout-queue + routing-key: user-balance-insufficient-checkout-routing-key + +# swagger 默认开启,在生产环境关闭,节省资源 +swagger: + enabled: true diff --git a/sonic-cow/server/src/main/resources/logback-spring.xml b/sonic-cow/server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..9e0bc7c --- /dev/null +++ b/sonic-cow/server/src/main/resources/logback-spring.xml @@ -0,0 +1,62 @@ + + + + + + + + + + ${DefaultPattern} + + + + + + + + ${ColorfulPattern} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sonic-cow/server/src/main/resources/mapper/GenImageTaskMapper.xml b/sonic-cow/server/src/main/resources/mapper/GenImageTaskMapper.xml new file mode 100644 index 0000000..1c5c35a --- /dev/null +++ b/sonic-cow/server/src/main/resources/mapper/GenImageTaskMapper.xml @@ -0,0 +1,31 @@ + + + + + + + update gen_image_task set + completed_count = #{completedCount}, + status = if(task_count = #{completedCount}, 'RELEASED', 'PENDING') + where id = #{id} and #{completedCount} > completed_count + + + + + update gen_image_task set + heart_beat_time = now(), + polling_count = polling_count + 1 + where batch_no = #{batchNo} + and status = 'PENDING' + + + + + + \ No newline at end of file diff --git a/sonic-cow/server/src/main/resources/mapper/VoiceChatRecordMapper.xml b/sonic-cow/server/src/main/resources/mapper/VoiceChatRecordMapper.xml new file mode 100644 index 0000000..5bd0b1a --- /dev/null +++ b/sonic-cow/server/src/main/resources/mapper/VoiceChatRecordMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-cow/server/src/main/resources/messages.properties b/sonic-cow/server/src/main/resources/messages.properties new file mode 100644 index 0000000..9e23e49 --- /dev/null +++ b/sonic-cow/server/src/main/resources/messages.properties @@ -0,0 +1,11 @@ +SYS_SYSTEM_EXCEPTION=System error +SYS_PERMISSION_DENIED=Insufficient permissions +GEN_CONTENT_LIMIT_ERROR=Maximum generation limit exceeded within 24 hours, please try again later +GEN_IMAGE_LIMIT_ERROR=Maximum of 10 image changes within 24 hours, please try again in {0} hours and {1} minutes +SOUND_ASR_ERROR=Audio recognition failed +AI_NOT_EXIST=AI does not exist +IMAGE_NOT_ILLEGAL=Image is not legal +USER_BALANCE_INSUFFICIENT=Your balance is insufficient! +NSWF_ERROR=Please remove the following sensitive content: {0} +GEN_IMAGE_TIMEOUT=Image generation timed out +MISS_PARAM_ERROR=Missing parameter \ No newline at end of file diff --git a/sonic-cow/server/src/main/resources/messages_en.properties b/sonic-cow/server/src/main/resources/messages_en.properties new file mode 100644 index 0000000..9e23e49 --- /dev/null +++ b/sonic-cow/server/src/main/resources/messages_en.properties @@ -0,0 +1,11 @@ +SYS_SYSTEM_EXCEPTION=System error +SYS_PERMISSION_DENIED=Insufficient permissions +GEN_CONTENT_LIMIT_ERROR=Maximum generation limit exceeded within 24 hours, please try again later +GEN_IMAGE_LIMIT_ERROR=Maximum of 10 image changes within 24 hours, please try again in {0} hours and {1} minutes +SOUND_ASR_ERROR=Audio recognition failed +AI_NOT_EXIST=AI does not exist +IMAGE_NOT_ILLEGAL=Image is not legal +USER_BALANCE_INSUFFICIENT=Your balance is insufficient! +NSWF_ERROR=Please remove the following sensitive content: {0} +GEN_IMAGE_TIMEOUT=Image generation timed out +MISS_PARAM_ERROR=Missing parameter \ No newline at end of file diff --git a/sonic-frog/.gitignore b/sonic-frog/.gitignore new file mode 100644 index 0000000..dd701a1 --- /dev/null +++ b/sonic-frog/.gitignore @@ -0,0 +1,28 @@ +/**/rebel.xml + +#target/ +target/ + +# IDEA # +.idea/ +*.iml + +# Eclipse # +.settings/ +.classpath +.project + +# Log # +logs/ +*.log% + +#mac +.DS_Store + +#svn +.svn +#reviewboardrc +.reviewboardrc +**/rest-client.private.env.json + +.lingma/ diff --git a/sonic-frog/.vscode/settings.json b/sonic-frog/.vscode/settings.json new file mode 100644 index 0000000..7b016a8 --- /dev/null +++ b/sonic-frog/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/sonic-frog/README.md b/sonic-frog/README.md new file mode 100644 index 0000000..d66ba8d --- /dev/null +++ b/sonic-frog/README.md @@ -0,0 +1,3 @@ +# sonic-frog + +综合业务 \ No newline at end of file diff --git a/sonic-frog/common/pom.xml b/sonic-frog/common/pom.xml new file mode 100644 index 0000000..f3decbc --- /dev/null +++ b/sonic-frog/common/pom.xml @@ -0,0 +1,58 @@ + + + + sonic-frog + com.sonic.frog + 1.0 + + 4.0.0 + + sonic-frog-common + jar + 1.0 + + + + com.sonic + common-lib + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + mysql + mysql-connector-java + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.google.guava + guava + + + com.alibaba + fastjson + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-pool2 + + + org.springframework.boot + spring-boot-starter-data-redis + + + diff --git a/sonic-frog/common/src/main/java/com/sonic/frog/common/GlobalConfig.java b/sonic-frog/common/src/main/java/com/sonic/frog/common/GlobalConfig.java new file mode 100644 index 0000000..6dc5714 --- /dev/null +++ b/sonic-frog/common/src/main/java/com/sonic/frog/common/GlobalConfig.java @@ -0,0 +1,115 @@ +package com.sonic.frog.common; + +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.AppRuntime; +import com.sonic.common.client.JobmanClient; +import com.sonic.common.client.impl.JobmanClientImpl; +import com.sonic.common.log.RequestLogAspect; +import com.sonic.common.rpc.HttpClient; +import com.sonic.common.rpc.RpcClient; +import com.sonic.common.rpc.RpcClientImpl; +import com.sonic.common.stat.FrequencyAlertInterceptor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.task.TaskExecutorBuilder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.task.TaskExecutor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.stream.Collectors; + +import static org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME; + +/** + * @author code + */ +@Slf4j +public class GlobalConfig { + @Value("${spring.application.name}") + private String appName; + @Value("${spring.application.id}") + private String appId; + + @Bean + ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() { + return new ScheduledThreadPoolExecutor(1); + } + + /** + * XXX Spring默认配置当有Executor的bean后不再装载TaskExecutor, 这里因为手动注册了 + * {@link #scheduledThreadPoolExecutor}会导致spring不再自动注册TaskExecutor, 因此需要去掉ConditionalOnMissingBean的限制. + * 参考{@link TaskExecutionAutoConfiguration#applicationTaskExecutor} + */ + @Bean(name = {APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME}) + public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { + return builder.build(); + } + + @Bean + public RequestLogAspect requestLogAspect() { + return new RequestLogAspect(); + } + + @Bean + AppRuntime appRuntime(ApplicationContext applicationContext) { + return AppRuntime.builder().appId(appId).appName(appName).applicationContext(applicationContext).build(); + } + + /** + * 用于配置接口访问频率超限告警, 接口频率配置参考application.yml中的web.frequency-alert.rules + * TODO 根据需要配置为输出到日志或者alertClient + * + * @return + */ + @Bean + public FrequencyAlertInterceptor frequencyAlertInterceptor(RuleConfig ruleConfig) { + return new FrequencyAlertInterceptor(ruleConfig.getAlertRuleMap(), + (request, frequencyAlert) -> log.warn("frequency alert, api frequency alert, url = {} alert = {}", + request.getRequestURI(), JSONObject.toJSONString(frequencyAlert))); + } + + @Bean + @Primary + public RpcClient rpcClient() { + return new RpcClientImpl(HttpClient.Config.EMPTY); + } + + @Bean + public JobmanClient jobmanClient(AppRuntime appRuntime, TaskExecutor taskExecutor, RedisTemplate redisTemplate) { + return new JobmanClientImpl.Builder().appRuntime(appRuntime).taskExecutor(taskExecutor).redisTemplate(redisTemplate).build(); + } + + @Data + @Component + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "web.frequency-alert") + public static class RuleConfig { + private List rules; + + public Map getAlertRuleMap() { + if (rules == null) { + return Collections.emptyMap(); + } + return rules.stream() + .map(e -> { + String[] split = e.split(":"); + return new AbstractMap.SimpleEntry<>(split[0], split[1]); + }).collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); + } + } +} diff --git a/sonic-frog/common/src/main/java/com/sonic/frog/common/MybatisPlusConfig.java b/sonic-frog/common/src/main/java/com/sonic/frog/common/MybatisPlusConfig.java new file mode 100644 index 0000000..eb78fb2 --- /dev/null +++ b/sonic-frog/common/src/main/java/com/sonic/frog/common/MybatisPlusConfig.java @@ -0,0 +1,19 @@ +package com.sonic.frog.common; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; + +@EnableTransactionManagement +@MapperScan("com.sonic.**.dao") +public class MybatisPlusConfig { + + @Bean + public PaginationInterceptor paginationInterceptor() { + PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); + paginationInterceptor.setDialectType("mysql"); + return paginationInterceptor; + } +} diff --git a/sonic-frog/lib/pom.xml b/sonic-frog/lib/pom.xml new file mode 100644 index 0000000..8e4b905 --- /dev/null +++ b/sonic-frog/lib/pom.xml @@ -0,0 +1,43 @@ + + + + sonic-frog + com.sonic.frog + 1.0 + + 4.0.0 + + com.sonic.frog + sonic-frog-lib + jar + 1.2-SNAPSHOT + + + + + com.sonic + common-lib + + + + log4j-api + org.apache.logging.log4j + + + com.alibaba + fastjson + + + + + + com.alibaba + fastjson + 1.2.83 + + + + + \ No newline at end of file diff --git a/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/AiChatInfoClient.java b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/AiChatInfoClient.java new file mode 100644 index 0000000..13af319 --- /dev/null +++ b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/AiChatInfoClient.java @@ -0,0 +1,74 @@ +package com.sonic.frog.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.frog.lib.output.AiChatInfoOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author code + */ +@Slf4j +@Service +public class AiChatInfoClient { + + private static final String GET_AI_CHT_INFO = "/api/ai-chat/info"; + private static final String UPDATE_IS_DEL_CHATTED = "/api/ai-chat/update-is-del-chatted"; + + private RpcClient rpcClient; + private String host; + + public AiChatInfoClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-frog-svc:8080"; + break; + case product: + default: + this.host = "http://prod-frog-svc:8080"; + } + } + + + /** + * 获取AI的基础信息 + * + * @param aiId + * @return + */ + public AiChatInfoOutput getAiChatInfo(Long userId, Long aiId) { + Map input = new HashMap<>(); + input.put("userId", userId); + input.put("aiId", aiId); + return rpcClient.post(host + GET_AI_CHT_INFO, input, new TypeReference>() { + }); + } + + /** + * 更新是否删除聊天消息 + * + * @return + */ + public Void updateIsDelChatted(Long userId, List aiIdList) { + Map input = new HashMap<>(); + input.put("userId", userId); + input.put("aiIdList", aiIdList); + return rpcClient.post(host + UPDATE_IS_DEL_CHATTED, input, new TypeReference>() { + }); + } + +} diff --git a/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/AiClient.java b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/AiClient.java new file mode 100644 index 0000000..6cb0828 --- /dev/null +++ b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/AiClient.java @@ -0,0 +1,56 @@ +package com.sonic.frog.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.frog.lib.output.AiInfoApiOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author code + */ +@Slf4j +@Service +public class AiClient { + + private static final String GET_AI_INFO = "/api/ai/get-info"; + + private RpcClient rpcClient; + private String host; + + public AiClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-frog-svc:8080"; + break; + case product: + default: + this.host = "http://prod-frog-svc:8080"; + } + } + + + /** + * 获取AI的基础信息 + * @param aiId + * @return + */ + public AiInfoApiOutput getAiInfo(Long aiId) { + Map input = new HashMap<>(); + input.put("aiId", aiId); + return rpcClient.post(host + GET_AI_INFO, input, new TypeReference>(){}); + } + +} diff --git a/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/AiUserAlbumClient.java b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/AiUserAlbumClient.java new file mode 100644 index 0000000..45e311c --- /dev/null +++ b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/AiUserAlbumClient.java @@ -0,0 +1,75 @@ +package com.sonic.frog.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author code + */ +@Slf4j +@Service +public class AiUserAlbumClient { + + private static final String GET_RANDOM_LOCK_IMAGE = "/api/ai-user-album/random-lock-image"; + private static final String USE_CREATE_COUNT = "/api/ai-user-album/use-create-count"; + + private RpcClient rpcClient; + private String host; + + public AiUserAlbumClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-frog-svc:8080"; + break; + case product: + default: + this.host = "http://prod-frog-svc:8080"; + } + } + + + /** + * 获取一张随机未解锁的图片 + * + * @param userId + * @param aiId + * @return + */ + public AIUserAlbumApiOutput getRandomLockImage(Long userId, Long aiId) { + Map input = new HashMap<>(); + input.put("userId", userId); + input.put("aiId", aiId); + return rpcClient.post(host + GET_RANDOM_LOCK_IMAGE, input, new TypeReference>() { + }); + } + + + /** + * 编辑形象或创建相册图片时,使用创作次数 + * + * @param userId + * @return + */ + public Void useCreateCount(Long userId) { + Map input = new HashMap<>(); + input.put("userId", userId); + return rpcClient.post(host + USE_CREATE_COUNT, input, new TypeReference>() { + }); + } + +} diff --git a/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/UserDeductionClient.java b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/UserDeductionClient.java new file mode 100644 index 0000000..77ca1fd --- /dev/null +++ b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/UserDeductionClient.java @@ -0,0 +1,57 @@ +package com.sonic.frog.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author code + */ +@Slf4j +@Service +public class UserDeductionClient { + + private static final String GET_TOTAL_DEDUCTION_AMOUNT = "/api/user/get-total-deduction-amount"; + + private RpcClient rpcClient; + private String host; + + public UserDeductionClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-frog-svc:8080"; + break; + case product: + default: + this.host = "http://prod-frog-svc:8080"; + } + } + + + /** + * 获取用户总预扣金额 + * + * @param userId + * @return + */ + public Long getTotalDeductionAmount(Long userId) { + Map input = new HashMap<>(); + input.put("userId", userId); + return rpcClient.post(host + GET_TOTAL_DEDUCTION_AMOUNT, input, new TypeReference>() { + }); + } + +} diff --git a/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/UserMemberClient.java b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/UserMemberClient.java new file mode 100644 index 0000000..812cd9b --- /dev/null +++ b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/client/UserMemberClient.java @@ -0,0 +1,89 @@ +package com.sonic.frog.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.Lists; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author code + */ +@Slf4j +@Service +public class UserMemberClient { + + private static final String USER_MEMBER_EXP_NOTIFY = "/api/user-member/exp-notify"; + private static final String USER_MEMBER_GIFT_USER_CREATE_COUNT = "/api/user-member/gift-user-create-count"; + + private RpcClient rpcClient; + private String host; + + public UserMemberClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-frog-svc:8080"; + break; + case product: + default: + this.host = "http://prod-frog-svc:8080"; + } + } + + + /** + * 用户会员到期通知 + * + * @param userId + */ + public void userMemberExpNotify(Long userId) { + Map input = new HashMap<>(); + input.put("userIdList", Lists.newArrayList(userId)); + rpcClient.post(host + USER_MEMBER_EXP_NOTIFY, input, new TypeReference>() { + }); + } + + /** + * 用户会员到期通知 + * + * @param userIdList + */ + public void userMemberExpNotify(List userIdList) { + Map input = new HashMap<>(); + input.put("userIdList", userIdList); + rpcClient.post(host + USER_MEMBER_EXP_NOTIFY, input, new TypeReference>() { + }); + } + + + /** + * 订阅会员或续订会员赠送用户创作次数 + * + * @param userId 用户id + * @param startTime 订阅或续订开始时间 + * @param expireTime 会员过期时 + */ + public void subMemberGiftUserCreateCount(Long userId, LocalDateTime startTime, LocalDateTime expireTime) { + Map input = new HashMap<>(); + input.put("userId", userId); + input.put("startTime", startTime); + input.put("expireTime", expireTime); + rpcClient.post(host + USER_MEMBER_GIFT_USER_CREATE_COUNT, input, new TypeReference>() { + }); + } + +} diff --git a/sonic-frog/lib/src/main/java/com/sonic/frog/lib/enums/HeartbeatLevelEnum.java b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/enums/HeartbeatLevelEnum.java new file mode 100644 index 0000000..7b0637d --- /dev/null +++ b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/enums/HeartbeatLevelEnum.java @@ -0,0 +1,45 @@ +package com.sonic.frog.lib.enums; + +import lombok.Getter; + +import java.math.BigDecimal; + +@Getter +public enum HeartbeatLevelEnum { + LEVEL_1(1, new BigDecimal("0.1"), "ACQUAINTANCE", "Initial acquaintance"), + LEVEL_2(2, new BigDecimal("0.3"), "ACQUAINTANCE", "Initial acquaintance"), + LEVEL_3(3, new BigDecimal("0.5"), "FRIEND", "Friend"), + LEVEL_4(4, new BigDecimal("1"), "FRIEND", "Friend"), + LEVEL_5(5, new BigDecimal("2"), "FLIRT", "Flirt"), + LEVEL_6(6, new BigDecimal("3"), "FLIRT", "Flirt"), + LEVEL_7(7, new BigDecimal("4"), "LOVE", "Love"), + LEVEL_8(8, new BigDecimal("5"), "LOVE", "Love"), + LEVEL_9(9, new BigDecimal("6"), "MARRIAGE", "Marriage"), + LEVEL_10(10, new BigDecimal("7"), "MARRIAGE", "Marriage"), + ; + + /** + * 对应等级数字num + */ + private final int num; + /** + * 对应等级24小时未聊天需要扣除的心动值 + */ + private BigDecimal subtractHeartbeatVal; + + /** + * 关系阶段代码 + */ + private String relationStage; + /** + * 关系阶段名称 + */ + private String relationStageName; + + HeartbeatLevelEnum(int num, BigDecimal subtractHeartbeatVal, String relationStage, String relationStageName) { + this.num = num; + this.subtractHeartbeatVal = subtractHeartbeatVal; + this.relationStage = relationStage; + this.relationStageName = relationStageName; + } +} diff --git a/sonic-frog/lib/src/main/java/com/sonic/frog/lib/output/AIUserAlbumApiOutput.java b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/output/AIUserAlbumApiOutput.java new file mode 100644 index 0000000..4dce6dc --- /dev/null +++ b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/output/AIUserAlbumApiOutput.java @@ -0,0 +1,39 @@ +package com.sonic.frog.lib.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class AIUserAlbumApiOutput { + + /** + * 相册ID + */ + private Long albumId; + + /** + * 用户ID + */ + private Long userId; + + /** + * 参数图片 + */ + private String imgUrl; + + /** + * 源图片地址 + */ + private String sourceImgUrl; + + /** + * 解锁价格 + */ + private Long unlockPrice; + +} diff --git a/sonic-frog/lib/src/main/java/com/sonic/frog/lib/output/AiChatInfoOutput.java b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/output/AiChatInfoOutput.java new file mode 100644 index 0000000..e99cf1a --- /dev/null +++ b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/output/AiChatInfoOutput.java @@ -0,0 +1,51 @@ +package com.sonic.frog.lib.output; + +import com.sonic.frog.lib.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:22 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiChatInfoOutput { + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("我是谁") + private String whoAmI; + + @ApiModelProperty("相识天数") + private Integer dayCount; + + @ApiModelProperty("是否通过meet相识") + private Boolean meet; + + @ApiModelProperty("心动等级") + private HeartbeatLevelEnum heartbeatLevel; + + @ApiModelProperty("关系阶段") + private String relationStage; + + @ApiModelProperty("解锁的心动等级列表") + private List unlockHearbeatLevelList; + +} diff --git a/sonic-frog/lib/src/main/java/com/sonic/frog/lib/output/AiInfoApiOutput.java b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/output/AiInfoApiOutput.java new file mode 100644 index 0000000..449edd3 --- /dev/null +++ b/sonic-frog/lib/src/main/java/com/sonic/frog/lib/output/AiInfoApiOutput.java @@ -0,0 +1,67 @@ +package com.sonic.frog.lib.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@Data +public class AiInfoApiOutput { + + @ApiModelProperty("ai所属用户id") + private Long userId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色code 对应ai_dict表中code") + private String roleCode; + + @ApiModelProperty("角色描述") + private String roleName; + + @ApiModelProperty("性格code 对应ai_dict表中code") + private String characterCode; + + @ApiModelProperty("性格描述") + private String characterName; + + @ApiModelProperty("标签code 对应ai_dict表中code") + private String tagCode; + + @ApiModelProperty("标签描述") + private String tagName; + + @ApiModelProperty("人物设定") + private String profile; + + @ApiModelProperty("人物设定 扩展字段") + private String userProfileExtJson; + + @ApiModelProperty("对话风格") + private String dialogueStyle; + + @ApiModelProperty("对话开场白") + private String dialoguePrologue; + + @ApiModelProperty("基图-首次创建AI时选择的形象图") + private String baseImageUrl; + + @ApiModelProperty("语音类型 第三方") + private String voiceType; + + @ApiModelProperty("对话-音高") + private String dialoguePitch; + + @ApiModelProperty("对话-语速") + private String dialogueSpeechRate; + +} diff --git a/sonic-frog/pom.xml b/sonic-frog/pom.xml new file mode 100644 index 0000000..d9cb553 --- /dev/null +++ b/sonic-frog/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + com.sonic + common-parent-pom + 1.0 + + + sonic-frog + + com.sonic.frog + pom + 1.0 + + + + 1.0.6 + 1.0 + + + + + + com.sonic + common-lib + ${common-lib.version} + + + com.sonic + dao-support-lib + ${dao-support-lib.version} + + + + + + + org.projectlombok + lombok + + + + + common + server + lib + + + + + + + + + + + + diff --git a/sonic-frog/server/pom.xml b/sonic-frog/server/pom.xml new file mode 100644 index 0000000..72e8091 --- /dev/null +++ b/sonic-frog/server/pom.xml @@ -0,0 +1,162 @@ + + + + sonic-frog + com.sonic.frog + 1.0 + + 4.0.0 + + sonic-frog-server + jar + + + + com.sonic.frog + sonic-frog-common + 1.0 + + + com.sonic.sdk + sonic-common-api + 1.0.1-SNAPSHOT + + + com.sonic + dao-support-lib + 1.0 + + + + com.sonic.bear + sonic-bear-lib + 1.1-SNAPSHOT + + + + com.sonic.shark + sonic-shark-lib + 1.0-SNAPSHOT + + + com.sonic.cow + sonic-cow-lib + 1.0-SNAPSHOT + + + + com.sonic.pigeon + sonic-pigeon-lib + 1.1-SNAPSHOT + + + + com.sonic.lion + sonic-lion-lib + 1.0-SNAPSHOT + + + + com.mashape.unirest + unirest-java + 1.4.9 + + + + + com.auth0 + jwks-rsa + 0.9.0 + + + io.jsonwebtoken + jjwt-api + 0.11.2 + + + io.jsonwebtoken + jjwt-impl + 0.11.2 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.2 + runtime + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + it.ozimov + embedded-redis + + + + + org.redisson + redisson-spring-boot-starter + 3.13.1 + + + + + org.springframework.amqp + spring-amqp + + + org.springframework.amqp + spring-rabbit + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + + + com.h2database + h2 + + + com.sonic.frog + sonic-frog-lib + 1.2-SNAPSHOT + compile + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pl.project13.maven + git-commit-id-plugin + + + false + false + + + + + diff --git a/sonic-frog/server/src/main/java/com/baomidou/mybatisplus/core/toolkit/Sequence.java b/sonic-frog/server/src/main/java/com/baomidou/mybatisplus/core/toolkit/Sequence.java new file mode 100644 index 0000000..d41c757 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/baomidou/mybatisplus/core/toolkit/Sequence.java @@ -0,0 +1,175 @@ +package com.baomidou.mybatisplus.core.toolkit; + +import lombok.extern.slf4j.Slf4j; + +import java.net.InetAddress; + +/** + * 原始链接 https://segmentfault.com/a/1190000020835840 + * 雪花算法分布式唯一ID生成器
+ * 每个机器号最高支持每秒‭65535个序列, 当秒序列不足时启用备份机器号, 若备份机器也不足时借用备份机器下一秒可用序列
+ * 53 bits 趋势自增ID结构如下: + *

+ * |00000000|00011111|11111111|11111111|11111111|11111111|11111111|11111111| + * |-----------|##########32bit 秒级时间戳##########|-----|-----------------| + * |--------------------------------------5bit机器位|xxxxx|-----------------| + * |-----------------------------------------16bit自增序列|xxxxxxxx|xxxxxxxx| + * + * @author: + * @date: 2021-12-30 + **/ +@Slf4j +public class Sequence { + /** + * 初始偏移时间戳 + */ + private final long OFFSET = 1546300800L; + + /** + * 机器id (0~15 保留 16~31作为备份机器) + */ + private long WORKER_ID; + /** + * 机器id所占位数 (5bit, 支持最大机器数 2^5 = 32) + */ + private final long WORKER_ID_BITS = 5L; + /** + * 自增序列所占位数 (16bit, 支持最大每秒生成 2^16 = ‭65536‬) + */ + private final long SEQUENCE_ID_BITS = 16L; + /** + * 机器id偏移位数 + */ + private final long WORKER_SHIFT_BITS = SEQUENCE_ID_BITS; + /** + * 自增序列偏移位数 + */ + private final long OFFSET_SHIFT_BITS = SEQUENCE_ID_BITS + WORKER_ID_BITS; + /** + * 机器标识最大值 (2^5 / 2 - 1 = 15) + */ + private final long WORKER_ID_MAX = ((1 << WORKER_ID_BITS) - 1) >> 1; + /** + * 备份机器ID开始位置 (2^5 / 2 = 16) + */ + private final long BACK_WORKER_ID_BEGIN = (1 << WORKER_ID_BITS) >> 1; + /** + * 自增序列最大值 (2^16 - 1 = ‭65535) + */ + private final long SEQUENCE_MAX = (1 << SEQUENCE_ID_BITS) - 1; + /** + * 发生时间回拨时容忍的最大回拨时间 (秒) + */ + private final long BACK_TIME_MAX = 1L; + + /** + * 上次生成ID的时间戳 (秒) + */ + private long lastTimestamp = 0L; + /** + * 当前秒内序列 (2^16) + */ + private long sequence = 0L; + /** + * 备份机器上次生成ID的时间戳 (秒) + */ + private long lastTimestampBak = 0L; + /** + * 备份机器当前秒内序列 (2^16) + */ + private long sequenceBak = 0L; + + public static int WORK_ID = 0; + + /** + * 使用ip第三位作为工作线程ID + */ + static { + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + String last = inetAddress.getHostAddress().split("\\.")[3]; + WORK_ID = Integer.valueOf(last) % 15; + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 私有构造函数禁止外部访问 + */ + public Sequence() { + WORKER_ID = WORK_ID; + } + + public Sequence(long workerId, long datacenterId) { + WORKER_ID = WORK_ID; + } + + /** + * 获取自增序列 + * + * @return long + */ + public long nextId() { + return nextId(SystemClock.now() / 1000); + } + + /** + * 主机器自增序列 + * + * @param timestamp 当前Unix时间戳 + * @return long + */ + private synchronized long nextId(long timestamp) { + // 时钟回拨检查 + if (timestamp < lastTimestamp) { + // 发生时钟回拨 + log.warn("时钟回拨, 启用备份机器ID: now: [{}] last: [{}]", timestamp, lastTimestamp); + return nextIdBackup(timestamp); + } + + // 开始下一秒 + if (timestamp != lastTimestamp) { + lastTimestamp = timestamp; + sequence = 0L; + } + if (0L == (++sequence & SEQUENCE_MAX)) { + // 秒内序列用尽 + log.warn("秒内[{}]序列用尽, 启用备份机器ID序列", timestamp); + sequence--; + return nextIdBackup(timestamp); + } + + return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | (WORKER_ID << WORKER_SHIFT_BITS) | sequence; + } + + /** + * 备份机器自增序列 + * + * @param timestamp timestamp 当前Unix时间戳 + * @return long + */ + private long nextIdBackup(long timestamp) { + if (timestamp < lastTimestampBak) { + if (lastTimestampBak - SystemClock.now() / 1000 <= BACK_TIME_MAX) { + timestamp = lastTimestampBak; + } else { + throw new RuntimeException(String.format("时钟回拨: now: [%d] last: [%d]", timestamp, lastTimestampBak)); + } + } + + if (timestamp != lastTimestampBak) { + lastTimestampBak = timestamp; + sequenceBak = 0L; + } + + if (0L == (++sequenceBak & SEQUENCE_MAX)) { + // 秒内序列用尽 + log.warn("秒内[{}]序列用尽, 备份机器ID借取下一秒序列", timestamp); + return nextIdBackup(timestamp + 1); + } + + return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | ((WORKER_ID ^ BACK_WORKER_ID_BEGIN) << WORKER_SHIFT_BITS) | sequenceBak; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/MainApplication.java b/sonic-frog/server/src/main/java/com/sonic/frog/MainApplication.java new file mode 100644 index 0000000..01ea472 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/MainApplication.java @@ -0,0 +1,33 @@ +package com.sonic.frog; + +import com.sonic.sdk.api.annotation.EnableDecrypt; +import com.sonic.sdk.api.annotation.EnableGatWayAuthScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableScheduling; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.Date; +import java.util.TimeZone; + +/** + * @author code + */ +@EnableSwagger2 +@EnableScheduling +@ComponentScan(value = {"com.sonic"}) +@SpringBootApplication +@EnableGatWayAuthScan(basePackages = "com.sonic.frog.controller") +@EnableDecrypt +public class MainApplication { + public static void main(String[] args) { + //打印时区 + System.out.println("===> 程序启动前,JVM 默认时区: " + TimeZone.getDefault().getID()); + System.out.println("===> 程序启动前,当前时间 (Date): " + new Date()); + SpringApplication.run(MainApplication.class, args); + //再次打印时区 + System.out.println("===> 程序启动后,JVM 默认时区: " + TimeZone.getDefault().getID()); + System.out.println("===> 程序启动后,当前时间 (Date): " + new Date()); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/client/AppleIdLoginValidateOauth2Client.java b/sonic-frog/server/src/main/java/com/sonic/frog/client/AppleIdLoginValidateOauth2Client.java new file mode 100644 index 0000000..5ba555b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/client/AppleIdLoginValidateOauth2Client.java @@ -0,0 +1,66 @@ +package com.sonic.frog.client; + +import com.alibaba.fastjson.annotation.JSONField; +import com.sonic.common.auth.domains.AppClientEnum; +import com.sonic.frog.domain.bo.ThirdAuthBo; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +public interface AppleIdLoginValidateOauth2Client { + + /** + * 验证token是否有效 + * @param accessToken + * @param appClient + * @return + */ + boolean isValid(String accessToken, AppClientEnum appClient); + + /** + * 解析token数据 + * @param idToken + * @return + */ + ThirdAuthBo parseIdByIdToken(String idToken); + + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + class CusJws { + private JwsHeader jwsHeader; + private JwsPayload jwsPayload; + private String signature; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + class JwsHeader { + private String kid; + private String alg; + } + + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + class JwsPayload { + public final static String ISS = "https://appleid.apple.com"; + + private String iss; + private String sub; + private String aud; + private Long exp; + private Long iat; + private String nonce; + private String email; + @JSONField(name = "email_verified") + private Boolean emailVerified; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/client/DiscordOauth2Client.java b/sonic-frog/server/src/main/java/com/sonic/frog/client/DiscordOauth2Client.java new file mode 100644 index 0000000..59f42d3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/client/DiscordOauth2Client.java @@ -0,0 +1,20 @@ +package com.sonic.frog.client; + + +import com.sonic.frog.domain.bo.ThirdAuthBo; + +/** + * google oauth2 + * + * @author code + */ +public interface DiscordOauth2Client { + + /** + * 根据 token 获取 discordId + * @param token + * @return + */ + ThirdAuthBo getDiscordIdByToken(String token); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/client/GoogleOauth2Client.java b/sonic-frog/server/src/main/java/com/sonic/frog/client/GoogleOauth2Client.java new file mode 100644 index 0000000..cb2c9ce --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/client/GoogleOauth2Client.java @@ -0,0 +1,19 @@ +package com.sonic.frog.client; + + +import com.sonic.frog.domain.bo.ThirdAuthBo; + +/** + * google oauth2 + * @author code + */ +public interface GoogleOauth2Client { + + /** + * 根据 token 获取 googleId + * @param token + * @return + */ + ThirdAuthBo getGoogleIdByToken(String token); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/client/dto/AppleReturnTokenResponse.java b/sonic-frog/server/src/main/java/com/sonic/frog/client/dto/AppleReturnTokenResponse.java new file mode 100644 index 0000000..d0e51a1 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/client/dto/AppleReturnTokenResponse.java @@ -0,0 +1,54 @@ +package com.sonic.frog.client.dto; + +public class AppleReturnTokenResponse { + + private String access_token; + + private String token_type; + + private Integer expires_in; + + private String refresh_token; + + private String id_token; + + public String getAccess_token() { + return access_token; + } + + public void setAccess_token(String access_token) { + this.access_token = access_token; + } + + public String getToken_type() { + return token_type; + } + + public void setToken_type(String token_type) { + this.token_type = token_type; + } + + public Integer getExpires_in() { + return expires_in; + } + + public void setExpires_in(Integer expires_in) { + this.expires_in = expires_in; + } + + public String getRefresh_token() { + return refresh_token; + } + + public void setRefresh_token(String refresh_token) { + this.refresh_token = refresh_token; + } + + public String getId_token() { + return id_token; + } + + public void setId_token(String id_token) { + this.id_token = id_token; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/client/dto/DiscordTokenResponse.java b/sonic-frog/server/src/main/java/com/sonic/frog/client/dto/DiscordTokenResponse.java new file mode 100644 index 0000000..c7e07ff --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/client/dto/DiscordTokenResponse.java @@ -0,0 +1,9 @@ +package com.sonic.frog.client.dto; + +import lombok.Data; + +@Data +public class DiscordTokenResponse { + private String error; + private String access_token; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/client/dto/DiscordUserResponse.java b/sonic-frog/server/src/main/java/com/sonic/frog/client/dto/DiscordUserResponse.java new file mode 100644 index 0000000..e259c01 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/client/dto/DiscordUserResponse.java @@ -0,0 +1,19 @@ +package com.sonic.frog.client.dto; + +import lombok.Data; + +@Data +public class DiscordUserResponse { + private String id; + private String username; + private String avatar; + /** + * 当前邮箱是否已被用户验证 + */ + private String verified; + /** + * 用户邮箱 + */ + private String email; + private String locale; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/client/impl/AppleIdLoginValidateOauth2ClientImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/client/impl/AppleIdLoginValidateOauth2ClientImpl.java new file mode 100644 index 0000000..92eb0a4 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/client/impl/AppleIdLoginValidateOauth2ClientImpl.java @@ -0,0 +1,203 @@ +package com.sonic.frog.client.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.auth0.jwk.InvalidPublicKeyException; +import com.auth0.jwk.Jwk; +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.domains.AppClientEnum; +import com.sonic.common.rpc.RequestParams; +import com.sonic.common.rpc.RpcClient; +import com.sonic.frog.client.AppleIdLoginValidateOauth2Client; +import com.sonic.frog.domain.bo.ThirdAuthBo; +import com.sonic.frog.utils.AppleThirdUtils; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.security.PublicKey; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @author code + */ +@Slf4j +@Service +public class AppleIdLoginValidateOauth2ClientImpl implements AppleIdLoginValidateOauth2Client { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RpcClient rpcClient; + @Autowired + private AppRuntime appRuntime; + + private static final int APPLE_ID_PUBLIC_KEY_EXPIRE = 24; + + @Value("${thirdLogin.apple.clientId}") + private String client_id; + @Value("${thirdLogin.apple.redirectUri}") + private String redirect_uri; + @Value("${thirdLogin.apple.aud}") + private String AUD; + + @Override + public boolean isValid(String accessToken, AppClientEnum appClient) { + if (AppClientEnum.IOS.equals(appClient)) { + //校验基本信息:nonce,iss,aud,exp + CusJws cusJws = this.getJws(accessToken); + if (cusJws == null) { + return false; + } + String aud = cusJws.getJwsPayload().getAud(); + if (!AUD.equalsIgnoreCase(aud)) { + return false; + } + //校验签名 + PublicKey publicKey = this.getAppleIdPublicKey(cusJws.getJwsHeader().getKid()); + return this.verifySignature2(publicKey, accessToken, aud, cusJws.getJwsPayload().getSub()); + } else if (AppClientEnum.WEB.equals(appClient)) { + String json = AppleThirdUtils.verify(accessToken); + if (StringUtils.isEmpty(json)) { + return false; + } + JSONObject jsonObject = JSON.parseObject(json); + //判断应用ID必须为配置中的 + if(!AppleThirdUtils.APPLICATION_ID.equals(jsonObject.getString("aud"))) { + return false; + } + return !StringUtils.isEmpty(jsonObject.getString("auth_time")); + } + return false; + } + + @Override + public ThirdAuthBo parseIdByIdToken(String idToken) { + //参考文档地址:https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple + ThirdAuthBo thirdAuthBo = new ThirdAuthBo(); + String json = AppleThirdUtils.verify(idToken); + log.info("===> parseIdByIdToken result : {}", json); + if(StringUtils.isEmpty(json)) { + return thirdAuthBo; + } + JSONObject jsonObject = JSON.parseObject(json); + String thirdId = jsonObject.getString("sub"); + thirdAuthBo.setThirdId(thirdId); + String emailVerified = jsonObject.getString("email_verified"); + if(Boolean.TRUE.toString().equals(emailVerified)) { + String email = jsonObject.getString("email"); + thirdAuthBo.setEmail(email); + } + return thirdAuthBo; + } + + + /** + * 参数验签 + * @param key + * @param jwt + * @param audience + * @param subject + * @return + */ + public boolean verifySignature2(PublicKey key, String jwt, String audience, String subject) { + JwtParser jwtParser = Jwts.parser().setSigningKey(key); + jwtParser.requireIssuer("https://appleid.apple.com"); + jwtParser.requireAudience(audience); + jwtParser.requireSubject(subject); + try { + Jws claim = jwtParser.parseClaimsJws(jwt); + return claim != null && claim.getBody().containsKey("auth_time"); + } catch (Exception e) { + log.error("===> verifySignature2 error : ", e); + return false; + } + } + + /** + * publicKey会本地缓存1天 + * @param kid + * @return + */ + private PublicKey getAppleIdPublicKey(String kid) { + String redisKey = appRuntime.buildPrefixKey("appleid", "publickey"); + String publicKeyStr = stringRedisTemplate.opsForValue().get(redisKey); + if (publicKeyStr == null) { + publicKeyStr = this.getAppleIdPublicKeyFromRemote(); + if (publicKeyStr == null) { + return null; + } + try { + PublicKey publicKey = this.publicKeyAdapter(publicKeyStr, kid); + stringRedisTemplate.opsForValue().set(redisKey, publicKeyStr, APPLE_ID_PUBLIC_KEY_EXPIRE, TimeUnit.HOURS); + return publicKey; + } catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + return this.publicKeyAdapter(publicKeyStr, kid); + } + + /** + * 将appleServer返回的publicKey转换成PublicKey对象 + * + * @param publicKeyStr + * @return + */ + private PublicKey publicKeyAdapter(String publicKeyStr, String kid) { + if (!StringUtils.hasText(publicKeyStr)) { + return null; + } + Map maps = (Map) JSON.parse(publicKeyStr); + List keys = (List) maps.get("keys"); + Map o = null; + for (Map key : keys) { + if (kid.equals(key.get("kid"))) { + o = key; + break; + } + } + Jwk jwa = Jwk.fromValues(o); + try { + PublicKey publicKey = jwa.getPublicKey(); + return publicKey; + } catch (InvalidPublicKeyException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 从appleServer获取publicKey + * + * @return + */ + private String getAppleIdPublicKeyFromRemote() { + return rpcClient.get("https://appleid.apple.com/auth/keys", + (bytes, stringListMap) -> bytes == null ? null : new String(bytes), + new RequestParams.FormParams(null, false, null, 1000L)); + } + + private CusJws getJws(String identityToken) { + String[] arrToken = identityToken.split("\\."); + if (arrToken.length != 3) { + return null; + } + Base64.Decoder decoder = Base64.getDecoder(); + JwsHeader jwsHeader = JSON.parseObject(new String(decoder.decode(arrToken[0])), JwsHeader.class); + JwsPayload jwsPayload = JSON.parseObject(new String(decoder.decode(arrToken[1])), JwsPayload.class); + return new CusJws(jwsHeader, jwsPayload, arrToken[2]); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/client/impl/DiscordOauth2ClientImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/client/impl/DiscordOauth2ClientImpl.java new file mode 100644 index 0000000..d44fe2e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/client/impl/DiscordOauth2ClientImpl.java @@ -0,0 +1,80 @@ +package com.sonic.frog.client.impl; + +import com.alibaba.fastjson.JSON; +import com.mashape.unirest.http.Unirest; +import com.sonic.frog.client.DiscordOauth2Client; +import com.sonic.frog.client.dto.DiscordTokenResponse; +import com.sonic.frog.client.dto.DiscordUserResponse; +import com.sonic.frog.domain.bo.ThirdAuthBo; +import com.sonic.frog.enums.BizResultCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class DiscordOauth2ClientImpl implements DiscordOauth2Client { + + @Value("${thirdLogin.discord.clientID}") + private String clientID ; + @Value("${thirdLogin.discord.clientSecret}") + private String clientSecret ; + @Value("${thirdLogin.discord.redirectUri}") + private String redirectUri ; + @Value("${thirdLogin.discord.scope}") + private String scope; + + @Override + public ThirdAuthBo getDiscordIdByToken(String code) { + ThirdAuthBo thirdAuthBo = new ThirdAuthBo(); + try { + //参考文档地址:https://discord.com/developers/docs/resources/user + //Step1: 根据code码来获取授权Token + String accessToken = getAccessTokenByCode(code); + //Step2: 根据授权Token获取用户自己的基础信息 + String response = Unirest.get("https://discord.com/api/v6/users/@me") + .header("Authorization", "Bearer "+ accessToken).asString().getBody(); + log.info("===> getDiscordIdByToken result : {}",response); + DiscordUserResponse discordUserResponse = JSON.parseObject(response, DiscordUserResponse.class); + + thirdAuthBo.setThirdId(discordUserResponse.getId()); + thirdAuthBo.setNickname(discordUserResponse.getUsername()); + //在这里我们只取用户认证过的邮箱,没有认证的邮箱是无效的,取出来也没用 + if(Boolean.TRUE.toString().equals(discordUserResponse.getVerified())) { + thirdAuthBo.setEmail(discordUserResponse.getEmail()); + } + return thirdAuthBo; + } catch (Exception e) { + log.error("discord get token by code error", e); + BizResultCode.DISCORD_NETWORK_ERROR.check(true); + } + return thirdAuthBo; + } + + /** + * 根据code码来获取访问用户基础信息的token + * @param code + * @return + */ + public String getAccessTokenByCode(String code) { + String accessToken = ""; + try { + String response = Unirest.post("https://discord.com/api/v6/oauth2/token") + .header("Content-Type", "application/x-www-form-urlencoded") + .field("client_id", clientID) + .field("client_secret", clientSecret) + .field("grant_type", "authorization_code") + .field("code", code) + .field("redirect_uri", redirectUri) + .field("scope", scope).asString().getBody(); + log.info("===> getAccessTokenByCode : {}", response); + DiscordTokenResponse discordTokenResponse = JSON.parseObject(response, DiscordTokenResponse.class); + return discordTokenResponse.getAccess_token(); + } catch (Exception e) { + log.error("===> discord get token by code error ", e); + BizResultCode.DISCORD_NETWORK_ERROR.check(true); + } + return accessToken; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/client/impl/GoogleOauth2ClientImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/client/impl/GoogleOauth2ClientImpl.java new file mode 100644 index 0000000..b73c8ca --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/client/impl/GoogleOauth2ClientImpl.java @@ -0,0 +1,68 @@ +package com.sonic.frog.client.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.common.exception.SysException; +import com.sonic.common.rpc.GlobalResultCode; +import com.sonic.frog.client.GoogleOauth2Client; +import com.sonic.frog.domain.bo.ThirdAuthBo; +import com.sonic.frog.enums.BizResultCode; +import com.sonic.frog.enums.Constants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +/** + * google oauth2 + * @author code + */ +@Slf4j +@Service +public class GoogleOauth2ClientImpl implements GoogleOauth2Client { + + @Value("${thirdLogin.gmail.authUrl}") + private String gmailAuthUrl; + @Value("${thirdLogin.gmail.client.webId}") + private String gmailWebClientId; + @Value("${thirdLogin.gmail.client.iosId}") + private String gmailIosClientId; + @Value("${thirdLogin.gmail.client.androidId}") + private String gmailAndroidClientId; + + @Override + public ThirdAuthBo getGoogleIdByToken(String token) { + String result; + try { + result = Unirest.get(gmailAuthUrl + token).asString().getBody(); + log.info("===> getGoogleIdByToken : {}", result); + } catch (UnirestException e) { + throw new SysException(GlobalResultCode.SYSTEM_EXCEPTION, e); + } + + log.info("===> getGoogleIdByToken result : {}", result); + JSONObject jsonObject = JSON.parseObject(result); + // 验证aud 是否为应用ID,只有当aud的值和我们配置的只匹配时才能确认时我们的应用,才可以使用 + BizResultCode.AUTH_FAIL.check(!gmailIosClientId.equals(jsonObject.getString(Constants.AUD)) + && !gmailWebClientId.equals(jsonObject.getString(Constants.AUD)) + && !gmailAndroidClientId.equals(jsonObject.getString(Constants.AUD))); + + String googleId = jsonObject.getString("sub"); + BizResultCode.GOOGLE_ID_GET_ERROR.check(StringUtils.isEmpty(googleId)); + + ThirdAuthBo bo = new ThirdAuthBo(); + //设置googleId + bo.setThirdId(googleId); + + //获取用户邮箱 我们只获取被用户认证通过的邮箱,未认证的邮箱没有太多作用,所以不获取 + String emailVerified = jsonObject.getString("email_verified"); + if(Boolean.TRUE.toString().equals(emailVerified)) { + String email = jsonObject.getString("email"); + //设置用户邮箱 + bo.setEmail(email); + } + return bo; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/config/Config.java b/sonic-frog/server/src/main/java/com/sonic/frog/config/Config.java new file mode 100644 index 0000000..48a39e6 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/config/Config.java @@ -0,0 +1,48 @@ +package com.sonic.frog.config; + +import com.sonic.common.AppRuntime; +import com.sonic.common.config.DefaultWebMvcConfig; +import com.sonic.common.exception.AbstractExceptionHandler; +import com.sonic.frog.common.GlobalConfig; +import com.sonic.frog.common.MybatisPlusConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; + +import java.util.function.Consumer; + +/** + * Server服务唯一的配置入口 + * TODO: Import的各种Config,按需使用(例如,如果不需要mysql,kafka消息,redis等,可以去掉对应的Config) + * + * @author code + */ +@Slf4j +@Configuration +@Import({GlobalConfig.class, DefaultWebMvcConfig.class, MybatisPlusConfig.class, RedisConfig.class, + EventConfig.class, SwaggerConfig.class}) +public class Config { + + /** + * XXX: ExceptionHandlerConfig 应该在 DefaultWebMvcConfig 前被初始. 否则 DefaultWebMvcConfig 的 exceptionHandlerHook 会生效 + */ + static class ExceptionHandlerConfig { + @Bean + @Profile({"!unittest && !local"}) + AbstractExceptionHandler.ExceptionHandlerHook exceptionHandlerHook(AppRuntime appRuntime) { + return new AbstractExceptionHandler.ExceptionHandlerHook() { + @Override + public String getAppId() { + return appRuntime.getAppId(); + } + + @Override + public Consumer getExceptionConsumer() { + return context -> log.error("未捕获内部异常, logText: {}", context.dumpRequest(), context.getException()); + } + }; + } + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/config/EventConfig.java b/sonic-frog/server/src/main/java/com/sonic/frog/config/EventConfig.java new file mode 100644 index 0000000..73d00c7 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/config/EventConfig.java @@ -0,0 +1,357 @@ +package com.sonic.frog.config; + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.ImmutableMap; +import com.rabbitmq.client.Channel; +import com.sonic.common.AppRuntime; +import com.sonic.common.event.*; +import com.sonic.common.utils.LogUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.annotation.RabbitListeners; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Payload; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * @author code + */ +@Slf4j +public class EventConfig { + + /** TODO: 定义 Event.BuildInScene */ + public final static String DEFAULT_SCENE = Event.BuildInScene.SONIC.getCode(); + /** TODO: DEFAULT_SCENE + "_" + appName */ + public final static String DEFAULT_MODULE = DEFAULT_SCENE + "_" + "frog"; + + public final static String PIGEON = DEFAULT_SCENE + "_" + "pigeon"; + + @Value("${mq.exchange}") + private String mqExchange; + @Value("${mq.default.queue}") + private String defaultQueue; + @Value("${mq.default.routing-key}") + private String defaultRoutingKey; + + @Value("${mq.aiImInfo.queue}") + private String aiImInfoQueue; + @Value("${mq.aiImInfo.routing-key}") + private String aiImInfoRoutingKey; + + @Value("${mq.calc-heartbeat-level.queue}") + private String calcHeartbeatLevelQueue; + @Value("${mq.calc-heartbeat-level.routing-key}") + private String calcHeartbeatLevelRoutingKey; + + @Value("${mq.calc-heartbeat-rank.queue}") + private String calcHeartbeatRankQueue; + @Value("${mq.calc-heartbeat-rank.routing-key}") + private String calcHeartbeatRankRoutingKey; + + @Value("${mq.aiChat.queue}") + private String aiChatQueue; + @Value("${mq.aiChat.routing-key}") + private String aiChatRoutingKey; + + @Value("${mq.aiChatToFrog.queue}") + private String aiChatToFrogQueue; + @Value("${mq.aiChatToFrog.routing-key}") + private String aiChatToFrogRoutingKey; + + @Value("${mq.subtract-heartbeat-val.queue}") + private String subtractHeartbeatValQueue; + @Value("${mq.subtract-heartbeat-val.routing-key}") + private String subtractHeartbeatValRoutingKey; + + @Value("${mq.calc-heartbeat-score.queue}") + private String calcHeartbeatScoreQueue; + @Value("${mq.calc-heartbeat-score.routing-key}") + private String calcHeartbeatScoreRoutingKey; + + @Value("${mq.ai-user-stat.queue}") + private String aiUserStatQueue; + @Value("${mq.ai-user-stat.routing-key}") + private String aiUserStatRoutingKey; + + @Value("${mq.user-deduction-stat.queue}") + private String userDeductionStatQueue; + @Value("${mq.user-deduction-stat.routing-key}") + private String userDeductionStatRoutingKey; + + @Value("${mq.user-balance-insufficient-checkout.queue}") + private String userBalanceInsufficientCheckoutQueue; + @Value("${mq.user-balance-insufficient-checkout.routing-key}") + private String userBalanceInsufficientCheckoutRoutingKey; + + @Value("${mq.ai-change.queue}") + private String aiChangeQueue; + @Value("${mq.ai-change.routing-key}") + private String aiChangeRoutingKey; + + @Configuration + @Profile("!unittest") + static class Listener { + @Autowired + EventConsumer eventConsumer; + + @RabbitListeners({ + @RabbitListener(queues = {"${mq.default.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.aiImInfo.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.calc-heartbeat-level.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.subtract-heartbeat-val.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.calc-heartbeat-score.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.ai-user-stat.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.calc-heartbeat-rank.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.user-deduction-stat.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.user-balance-insufficient-checkout.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.ai-change.queue}"}, concurrency = "2"), + @RabbitListener(queues = {"${mq.aiChatToFrog.queue}"}, concurrency = "2"), + }) + public void process(@Payload Message message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) { + try { + LogUtils.setTraceId(); + byte[] msgBody = message.getBody(); + String contentEncoding = message.getMessageProperties().getContentEncoding(); + String bodyStr = new String(msgBody, Charset.forName(ObjectUtils.defaultIfNull(contentEncoding, "UTF-8"))); + MessageProperties pro = message.getMessageProperties(); + if (log.isDebugEnabled()) { + log.info("receive msg routingKey:{}->consumerQueue:{}->redelivered:{}->body:{}", + pro.getReceivedRoutingKey(), pro.getConsumerQueue(), pro.getRedelivered(), bodyStr); + } + eventConsumer.onEvent(bodyStr, ImmutableMap.of("record", JSON.toJSONString(message))); + + //消息确认,multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息 + channel.basicAck(deliveryTag, false); + } catch (Exception e) { + log.error("===> mq handler error, message: {}", JSON.toJSONString(message), e); + //消息拒绝,//requeue=true,表示将消息重新放入到队列中,false:表示直接从队列中删除,此时和basicAck(long deliveryTag, false)的效果一样 + try { + channel.basicReject(deliveryTag,true); + } catch (IOException ioException) { + log.error("channel.basicReject error. message: {}, deliveryTag: {}", JSON.toJSONString(message), deliveryTag, ioException); + } + } finally { + LogUtils.removeTraceId(); + } + } + + } + + @Bean + EventProducer eventProducer(AppRuntime appRuntime, RabbitTemplate rabbitTemplate, TaskExecutor taskExecutor) { + BiConsumer callback = (event, meta) -> {}; + return new RabbitmqEventProducer(rabbitTemplate, DEFAULT_MODULE, DEFAULT_SCENE, appRuntime.getAppId(), + RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, defaultRoutingKey), taskExecutor, callback); + } + + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta aiImInfoMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, aiImInfoRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta calcHeartbeatLevelMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, calcHeartbeatLevelRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta aiChatMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, aiChatRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta aiChatToFrogMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, aiChatToFrogRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta subtractHeartbeatValMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, subtractHeartbeatValRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta calcHeartbeatScoreMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, calcHeartbeatScoreRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta calcHeartbeatRankMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, calcHeartbeatRankRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta aiUserStatMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, aiUserStatRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta userDeductionStatMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, userDeductionStatRoutingKey); + } + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta userBalanceInsufficientCheckoutMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, userBalanceInsufficientCheckoutRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta aiChangeMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, aiChangeRoutingKey); + } + + + @Bean + EventConsumer eventConsumer(AppRuntime appRuntime, EventHandlerRepository eventHandlerRepository) { + Consumer callback = (eventWrapper) -> {}; + return new DefaultEventConsumer(appRuntime.getAppName(), eventHandlerRepository, callback); + } + + @Bean + EventHandlerRepository eventHandlerRepository() { + return new EventHandlerRepository((ex, logText) -> log.error("MQ,handle error {}", logText, ex)); + } + + @Bean + public DirectExchange messageServerExchange(){ + return new DirectExchange(mqExchange); + } + + + @Bean + public Binding bindingDefaultQueueExchange(DirectExchange exchange, Queue defaultQueue) { + return bindingExchange(exchange, defaultQueue, defaultRoutingKey); + } + + @Bean + public Binding bindingAiImInfoQueueExchange(DirectExchange exchange, Queue aiImInfoQueue) { + return bindingExchange(exchange, aiImInfoQueue, aiImInfoRoutingKey); + } + + @Bean + public Binding bindingAiChatQueueExchange(DirectExchange exchange) { + return bindingExchange(exchange, aiChatQueue(), aiChatRoutingKey); + } + + @Bean + public Binding bindingAiChatToFrogQueueExchange(DirectExchange exchange) { + return bindingExchange(exchange, aiChatToFrogQueue(), aiChatToFrogRoutingKey); + } + + @Bean + public Binding bindingCalcHeartbeatLevelQueueExchange(DirectExchange exchange, Queue calcHeartbeatLevelQueue) { + return bindingExchange(exchange, calcHeartbeatLevelQueue, calcHeartbeatLevelRoutingKey); + } + + @Bean + public Binding bindingCalcHeartbeatRankQueueExchange(DirectExchange exchange, Queue calcHeartbeatRankQueue) { + return bindingExchange(exchange, calcHeartbeatRankQueue, calcHeartbeatRankRoutingKey); + } + + @Bean + public Binding bindingSubtractHeartbeatValQueueExchange(DirectExchange exchange, Queue subtractHeartbeatValQueue) { + return bindingExchange(exchange, subtractHeartbeatValQueue, subtractHeartbeatValRoutingKey); + } + + @Bean + public Binding bindingCalcHeartbeatScoreQueueExchange(DirectExchange exchange, Queue calcHeartbeatScoreQueue) { + return bindingExchange(exchange, calcHeartbeatScoreQueue, calcHeartbeatScoreRoutingKey); + } + + @Bean + public Binding bindingAiUserStatQueueExchange(DirectExchange exchange, Queue aiUserStatQueue) { + return bindingExchange(exchange, aiUserStatQueue, aiUserStatRoutingKey); + } + @Bean + public Binding bindingUserDeductionStatQueueExchange(DirectExchange exchange, Queue userDeductionStatQueue) { + return bindingExchange(exchange, userDeductionStatQueue, userDeductionStatRoutingKey); + } + @Bean + public Binding bindingUserBalanceInsufficientCheckoutQueueExchange(DirectExchange exchange, Queue userBalanceInsufficientCheckoutQueue) { + return bindingExchange(exchange, userBalanceInsufficientCheckoutQueue, userBalanceInsufficientCheckoutRoutingKey); + } + + @Bean + public Binding bindingAiChangeQueueExchange(DirectExchange exchange, Queue aiChangeQueue) { + return bindingExchange(exchange, aiChangeQueue, aiChangeRoutingKey); + } + + + @Bean + public Queue defaultQueue() { + return new Queue(defaultQueue,true); + } + + @Bean + public Queue aiImInfoQueue() { + return new Queue(aiImInfoQueue,true); + } + + @Bean + public Queue calcHeartbeatLevelQueue() { + return new Queue(calcHeartbeatLevelQueue,true); + } + + @Bean + public Queue calcHeartbeatRankQueue() { + return new Queue(calcHeartbeatRankQueue,true); + } + + @Bean + public Queue aiChatQueue() { + return new Queue(aiChatQueue,true); + } + + @Bean + public Queue aiChatToFrogQueue() { + return new Queue(aiChatToFrogQueue,true); + } + + @Bean + public Queue subtractHeartbeatValQueue() { + return new Queue(subtractHeartbeatValQueue,true); + } + + @Bean + public Queue calcHeartbeatScoreQueue() { + return new Queue(calcHeartbeatScoreQueue,true); + } + + @Bean + public Queue aiUserStatQueue() { + return new Queue(aiUserStatQueue,true); + } + + @Bean + public Queue userDeductionStatQueue() { + return new Queue(userDeductionStatQueue,true); + } + @Bean + public Queue userBalanceInsufficientCheckoutQueue() { + return new Queue(userBalanceInsufficientCheckoutQueue,true); + } + + @Bean + public Queue aiChangeQueue() { + return new Queue(aiChangeQueue,true); + } + + public Binding bindingExchange(DirectExchange exchange, Queue queueMessage, String routingKey) { + return BindingBuilder.bind(queueMessage).to(exchange).with(routingKey); + } + +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/config/RedisConfig.java b/sonic-frog/server/src/main/java/com/sonic/frog/config/RedisConfig.java new file mode 100644 index 0000000..8dac409 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/config/RedisConfig.java @@ -0,0 +1,39 @@ +package com.sonic.frog.config; + +import com.sonic.common.utils.RedisLock; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * 有关Redis的配置 + * + * TODO: 如果不需要Redis, 可以删除该文件并且在{@link Config}类@Import的注解中移除对RedisConfig的引用 + */ +@Slf4j +public class RedisConfig { + + /** + * redisWrapper用于分布式锁RedisLock + * + * @param redisTemplate + * @return + */ + @Bean + RedisLock.RedisWrapper redisWrapper(RedisTemplate redisTemplate) { + return new RedisLock.RedisWrapper() { + @Override + public void delete(String key) { + redisTemplate.delete(key); + } + + @Override + public boolean lock(String key, String value, long expireMills) { + return redisTemplate.opsForValue().setIfAbsent(key, value, expireMills, TimeUnit.MILLISECONDS); + } + }; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/config/RedissonConfig.java b/sonic-frog/server/src/main/java/com/sonic/frog/config/RedissonConfig.java new file mode 100644 index 0000000..f57d081 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/config/RedissonConfig.java @@ -0,0 +1,52 @@ +package com.sonic.frog.config; + +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + + /** + * 配置 RedissonClient,支持单机和集群模式 + */ + @Bean + public RedissonClient redissonClient(RedisProperties redisProperties) { + Config config = new Config(); + + // 设置 Netty 线程数,使用默认值 0(Redisson 默认使用 CPU 核心数的两倍) + config.setNettyThreads(0); + + // 判断是否配置了集群节点 + if (redisProperties.getCluster() != null && redisProperties.getCluster().getNodes() != null + && !redisProperties.getCluster().getNodes().isEmpty()) { + // 集群模式配置 + config.useClusterServers() + .setScanInterval(2000) // 集群状态扫描间隔,单位毫秒 + .addNodeAddress(redisProperties.getCluster().getNodes().stream() + .map(node -> "redis://" + node) + .toArray(String[]::new)) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null); + } else { + // 单机模式配置 + config.useSingleServer() + .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort()) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null) + .setDatabase(redisProperties.getDatabase()); + } + + // 如果启用了 SSL,设置 SSL 相关配置 + if (redisProperties.isSsl()) { + config.useSingleServer().setSslEnableEndpointIdentification(false); // 禁用主机名验证 + // 集群模式下,Redisson 会自动应用 SSL 配置 + } + + return Redisson.create(config); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/config/RestTemplateConfig.java b/sonic-frog/server/src/main/java/com/sonic/frog/config/RestTemplateConfig.java new file mode 100644 index 0000000..d33fa39 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/config/RestTemplateConfig.java @@ -0,0 +1,24 @@ +package com.sonic.frog.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(ClientHttpRequestFactory factory){ + return new RestTemplate(factory); + } + + @Bean + public ClientHttpRequestFactory simpleClientHttpRequestFactory(){ + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setReadTimeout(5000);//单位为ms + factory.setConnectTimeout(5000);//单位为ms + return factory; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/config/ResultCode.java b/sonic-frog/server/src/main/java/com/sonic/frog/config/ResultCode.java new file mode 100644 index 0000000..1250dc7 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/config/ResultCode.java @@ -0,0 +1,43 @@ +package com.sonic.frog.config; + +import com.sonic.common.rpc.ApiResultCode; +import lombok.Getter; + +/** + * @author code + */ +@Getter +public enum ResultCode implements ApiResultCode { + + /** + * TODO: 可以在此处扩展服务自身需要用到的错误码信息 + */ + BIZ_ERROR1("0000", "业务异常1"), + DEMO_CREATED_FAIL("0001", "新增Demo实体失败"); + + private final String errorCode; + private final String errorMsg; + + ResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/config/SsoConfig.java b/sonic-frog/server/src/main/java/com/sonic/frog/config/SsoConfig.java new file mode 100644 index 0000000..83c71ea --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/config/SsoConfig.java @@ -0,0 +1,16 @@ +package com.sonic.frog.config; + +import com.sonic.common.auth.GateWaySessionInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SsoConfig { + + + @Bean + public GateWaySessionInterceptor gateWaySessionInterceptor() { + return new GateWaySessionInterceptor(); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/config/SwaggerConfig.java b/sonic-frog/server/src/main/java/com/sonic/frog/config/SwaggerConfig.java new file mode 100644 index 0000000..fa8c948 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/config/SwaggerConfig.java @@ -0,0 +1,110 @@ +package com.sonic.frog.config; + + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import springfox.documentation.RequestHandler; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; + +/** + * swagger配置 + * @author code + */ +@Slf4j +@EnableSwagger2 +public class SwaggerConfig { + + private static final String SPLIT = ","; + + @Value("${swagger.enabled:false}") + private Boolean swaggerEnabled; + + @Value("${swagger.base.package}") + private String basePackageValue; + + private final String DEFAULT_BASE_PACKAGE = "com.sonic.bs.controller"; + + + public static Predicate basePackage(final String basePackage) { + return input -> declaringClass(input).transform(handlerPackage(basePackage)).or(true); + } + + private static Optional> declaringClass(RequestHandler input) { + return Optional.fromNullable(input.declaringClass()); + } + + private static Function, Boolean> handlerPackage(final String basePackage) { + return input -> { + // 循环判断匹配 + for (String strPackage : basePackage.split(SPLIT)) { + boolean isMatch = input.getPackage().getName().startsWith(strPackage); + if (isMatch) { + return true; + } + } + return false; + }; + } + + @Bean + public Docket createRestApi() { + basePackageValue = null == basePackageValue || ("").equals(basePackageValue) ? DEFAULT_BASE_PACKAGE : basePackageValue; + log.info("===> swagger base package : ", basePackageValue); + + //在配置好的配置类中增加此段代码即可 + ParameterBuilder ticketPar = new ParameterBuilder(); + List pars = new ArrayList(); + ticketPar.name("_tk_") + //name表示名称,description表示描述 + .description("token") + .modelRef(new ModelRef("string")) + .parameterType("header") + .required(false) + .defaultValue("") + .build();//required表示是否必填,defaultvalue表示默认值 + + //添加完此处一定要把下边的带***的也加上否则不生效 + pars.add(ticketPar.build()); + + + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .enable(swaggerEnabled) + .select() + .apis(basePackage(basePackageValue)) + .paths(PathSelectors.any()) + .build() + //************把消息头添加 + .globalOperationParameters(pars); + } + + /** + * TODO: 更改文案配置 + * @return + */ + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("epal") + .description("epal API") + .version("1.0") + .contact(new Contact("epal", "", "admin.epal.gg")) + .build(); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/AiApi.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/AiApi.java new file mode 100644 index 0000000..0070129 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/AiApi.java @@ -0,0 +1,35 @@ +package com.sonic.frog.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.AiInfoApiInput; +import com.sonic.frog.domain.output.AiInfoApiOutput; +import com.sonic.frog.service.AiUserService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * AI接口 + * + * @author code + */ +@RestController +@Slf4j +public class AiApi { + + @Autowired + private AiUserService aiUserService; + + @IgnoreAuth + @ApiOperation(value = "获取AI基础信息", tags = {"API-接口"}) + @PostMapping("/api/ai/get-info") + public Result getAiInfo(@RequestBody AiInfoApiInput input) { + return Result.success(aiUserService.getAiUserInfo(input.getAiId())); + } + +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/AiChatInfoApi.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/AiChatInfoApi.java new file mode 100644 index 0000000..720a3aa --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/AiChatInfoApi.java @@ -0,0 +1,47 @@ +package com.sonic.frog.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.GetRandomLockImageInput; +import com.sonic.frog.domain.input.UpdateIsDelChattedInput; +import com.sonic.frog.domain.output.AiChatInfoOutput; +import com.sonic.frog.service.AiChatInfoService; +import com.sonic.frog.service.ChatSetService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * AI接口 + * + * @author code + */ +@RestController +@Slf4j +public class AiChatInfoApi { + + @Autowired + private AiChatInfoService aiChatInfoService; + @Autowired + private ChatSetService chatSetService; + + @IgnoreAuth + @ApiOperation(value = "获取聊天设置基础信息", tags = {"API-接口"}) + @PostMapping("/api/ai-chat/info") + public Result getAiChatInfo(@RequestBody GetRandomLockImageInput input) { + return Result.success(aiChatInfoService.getAiChatInfo(input.getUserId(), input.getAiId())); + } + + @IgnoreAuth + @ApiOperation(value = "获取聊天设置基础信息", tags = {"API-接口"}) + @PostMapping("/api/ai-chat/update-is-del-chatted") + public Result updateIsDelChatted(@RequestBody UpdateIsDelChattedInput input) { + chatSetService.updateIsDelChatted(input.getUserId(), input.getAiIdList()); + return Result.success(); + } + +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/AiUserAlbumApi.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/AiUserAlbumApi.java new file mode 100644 index 0000000..0d6ab5b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/AiUserAlbumApi.java @@ -0,0 +1,48 @@ +package com.sonic.frog.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.GetRandomLockImageInput; +import com.sonic.frog.domain.input.UseUserCreateCountInput; +import com.sonic.frog.service.AiUserAlbumService; +import com.sonic.frog.service.UserCreateCountStatService; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.security.InvalidKeyException; + +/** + * AI接口 + * + * @author code + */ +@RestController +@Slf4j +public class AiUserAlbumApi { + + @Autowired + private AiUserAlbumService aiUserAlbumService; + @Autowired + private UserCreateCountStatService userCreateCountStatService; + + @IgnoreAuth + @ApiOperation(value = "获取未解锁的图片", tags = {"API-接口"}) + @PostMapping("/api/ai-user-album/random-lock-image") + public Result getRandomLockImage(@RequestBody GetRandomLockImageInput input) throws InvalidKeyException { + return Result.success(aiUserAlbumService.getRandomLockImage(input)); + } + + @IgnoreAuth + @ApiOperation(value = "编辑形象或创建相册图片时,使用创作次数", tags = {"API-接口"}) + @PostMapping("/api/ai-user-album/use-create-count") + public Result useUserCreateCount(@RequestBody UseUserCreateCountInput input) throws InvalidKeyException { + userCreateCountStatService.useUserCreateCount(input.getUserId()); + return Result.success(); + } +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/UserDeductionApi.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/UserDeductionApi.java new file mode 100644 index 0000000..4e53631 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/UserDeductionApi.java @@ -0,0 +1,37 @@ +package com.sonic.frog.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.AiInfoApiInput; +import com.sonic.frog.domain.input.GetTotalDeductionAmountInput; +import com.sonic.frog.domain.output.AiInfoApiOutput; +import com.sonic.frog.service.AiUserService; +import com.sonic.frog.service.UserDeductionStatService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * AI接口 + * + * @author code + */ +@RestController +@Slf4j +public class UserDeductionApi { + + @Autowired + private UserDeductionStatService userDeductionStatService; + + @IgnoreAuth + @ApiOperation(value = "获取用户总预扣金额", tags = {"API-接口"}) + @PostMapping("/api/user/get-total-deduction-amount") + public Result getTotalDeductionAmount(@RequestBody GetTotalDeductionAmountInput input) { + return Result.success(userDeductionStatService.getTotalDeductionAmount(input.getUserId())); + } + +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/UserMemberApi.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/UserMemberApi.java new file mode 100644 index 0000000..b29e078 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/api/UserMemberApi.java @@ -0,0 +1,48 @@ +package com.sonic.frog.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.SubMemberGiftUserCreateCountInput; +import com.sonic.frog.domain.input.UserIdInput; +import com.sonic.frog.service.ChatSetService; +import com.sonic.frog.service.UserCreateCountStatService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * AI接口 + * + * @author code + */ +@RestController +@Slf4j +public class UserMemberApi { + + @Autowired + private ChatSetService chatSetService; + @Autowired + private UserCreateCountStatService userCreateCountStatService; + + @IgnoreAuth + @ApiOperation(value = "用户会员过期通知", tags = {"API-接口"}) + @PostMapping("/api/user-member/exp-notify") + public Result userMemberExpNotify(@RequestBody UserIdInput input) { + //关闭掉自动播放语音的开关 + chatSetService.closeAutoPlayVoice(input.getUserIdList()); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "订阅会员或续订时,增加用户会员创作次数", tags = {"API-接口"}) + @PostMapping("/api/user-member/gift-user-create-count") + public Result subMemberGiftUserCreateCount(@RequestBody SubMemberGiftUserCreateCountInput input) { + userCreateCountStatService.subMemberGiftUserCreateCount(input.getUserId(),input.getStartTime(),input.getExpireTime()); + return Result.success(); + } + +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/mock/MockController.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/mock/MockController.java new file mode 100644 index 0000000..d8199c3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/mock/MockController.java @@ -0,0 +1,160 @@ +package com.sonic.frog.controller.mock; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.AiUserExt; +import com.sonic.frog.domain.entity.AiUserHeartbeatRelation; +import com.sonic.frog.domain.input.GetRandomLockImageInput; +import com.sonic.frog.domain.input.MockInput; +import com.sonic.frog.domain.output.SignInRoundOutput; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.RedisKeyUtils; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.security.InvalidKeyException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Mock测试 + */ +@Slf4j +@RestController +public class MockController { + + @Autowired + private SignInRecordService signInRecordService; + @Autowired + private SignInStatSearchService signInStatSearchService; + @Autowired + private RankService rankService; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private AiUserAlbumService aiUserAlbumService; + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private MockService mockService; + @Autowired + private AiUserExtService aiUserExtService; + + @IgnoreAuth + @ApiOperation(value = "签到", tags = {"Mock-签到"}) + @PostMapping("/mock/si/asi") + public Result signIn(@ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + session.setUserId(1L); + return Result.success(signInRecordService.signIn(session.getUserId())); + } + + @IgnoreAuth + @ApiOperation(value = "七天签到列表", tags = {"Mock-签到"}) + @PostMapping("/mock/si/list") + public Result signInList(@ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + session.setUserId(1L); + return Result.success(signInStatSearchService.signInList(session.getUserId())); + } + + @IgnoreAuth + @ApiOperation(value = "聊天热榜定时任务", tags = {"Mock-签到"}) + @PostMapping("/mock/rank/chat") + public Result chatJob() { + return Result.success(rankService.chatRankJob()); + } + + @IgnoreAuth + @ApiOperation(value = "心动值榜定时任务", tags = {"Mock-签到"}) + @PostMapping("/mock/rank/heartbeat") + public Result aiHeartbeatRankJob() { + return Result.success(rankService.aiHeartbeatRankJob()); + } + + @IgnoreAuth + @ApiOperation(value = "初始化用户与ai的心动等级", tags = {"Mock-签到"}) + @PostMapping("/mock/heartbeat/initUserAiHeartbeatLevel") + public Result initUserAiHeartbeatLevel() { + List list = aiUserHeartbeatRelationService.list(Wrappers.lambdaQuery().gt(AiUserHeartbeatRelation::getHeartbeatVal, 0)); + for (AiUserHeartbeatRelation aiUserHeartbeatRelation : list) { + if (aiUserHeartbeatRelation.getHeartbeatLevel() == null) { + continue; + } + redisTemplate.delete(redisKeyUtils.aiUserHeartbeatLevelCacheKey(aiUserHeartbeatRelation.getUserId(), aiUserHeartbeatRelation.getAiId())); + stringRedisTemplate.delete(redisKeyUtils.aiUserHeartbeatLevelCacheKey(aiUserHeartbeatRelation.getUserId(), aiUserHeartbeatRelation.getAiId())); + stringRedisTemplate.opsForValue().set(redisKeyUtils.aiUserHeartbeatLevelCacheKey(aiUserHeartbeatRelation.getUserId(), aiUserHeartbeatRelation.getAiId()), aiUserHeartbeatRelation.getHeartbeatLevel().name(), 3650, TimeUnit.DAYS); + } + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "获取未解锁的图片", tags = {"Mock-相册"}) + @PostMapping("/mock/ai-user-album/random-lock-image") + public Result getRandomLockImage(@RequestBody GetRandomLockImageInput input) throws InvalidKeyException { + return Result.success(aiUserAlbumService.getRandomLockImage(input)); + } + + @IgnoreAuth + @ApiOperation(value = "初始化ai开场白声音和辅助聊天", tags = {"Mock-相册"}) + @PostMapping("/mock/initAiDialoguePrologueSoundAndSupportingContent") + public Result initAiDialoguePrologueSoundAndSupportingContent(@RequestBody GetRandomLockImageInput input) throws InvalidKeyException { + //获取未删除的ai + List list = aiUserSearchService.list(Wrappers.lambdaQuery().eq(AiUser::getIsDelete, false)); + //发送MQ初始化 + for (AiUser aiUser : list) { + commonSendMqService.aiChangeMq(aiUser.getAiId(), true, true); + } + return Result.success(); + } + + + @IgnoreAuth + @ApiOperation(value = "更新AI基础信息", tags = {"Mock-相册"}) + @PostMapping("/mock/updateAiUserExt") + public Result updateAiUserExt(@RequestBody MockInput input) throws UnirestException { + if(input.getAiId() != null) { + mockService.updateAiUserExt(input.getAiId()); + return Result.success(); + } + if(CollectionUtils.isNotEmpty(input.getAiIdList())) { + //查询出未删除的ai进行处理 + List list = aiUserExtService.list(Wrappers.lambdaQuery() + .eq(AiUserExt::getIsDelete, false) + .in(CollectionUtils.isNotEmpty(input.getAiIdList()), AiUserExt::getAiId, input.getAiIdList())); + for (AiUserExt aiUserExt : list) { + mockService.updateAiUserExt(aiUserExt.getAiId()); + } + return Result.success(); + } + //查询出未删除的ai进行处理 + List list = aiUserExtService.list(Wrappers.lambdaQuery() + .eq(AiUserExt::getIsDelete, false) + .isNull(AiUserExt::getUserProfileExtJson)); + for (AiUserExt aiUserExt : list) { + mockService.updateAiUserExt(aiUserExt.getAiId()); + } + return Result.success(); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/openapi/OpenApiWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/openapi/OpenApiWeb.java new file mode 100644 index 0000000..1340ae6 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/openapi/OpenApiWeb.java @@ -0,0 +1,35 @@ +package com.sonic.frog.controller.openapi; + +import com.sonic.bear.lib.client.UserSessionClient; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.TokenCheckInput; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Web-Dict管理 + * + * @author code + */ +@RestController +@Slf4j +public class OpenApiWeb { + + @Autowired + private UserSessionClient userSessionClient; + + @IgnoreAuth + @ApiOperation(value = "用户token认证", tags = {"OpenApi接口"}) + @PostMapping("/web/auth/token-check") + public Result authTokenCheck(@RequestBody TokenCheckInput input) { + Session session = userSessionClient.touchSession(input.getToken()); + return Result.success(session); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/probe/ProbeController.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/probe/ProbeController.java new file mode 100644 index 0000000..072494a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/probe/ProbeController.java @@ -0,0 +1,23 @@ +package com.sonic.frog.controller.probe; + +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 探测接口 + */ +@RestController +@Slf4j +public class ProbeController { + + @IgnoreAuth + @ApiOperation(value = "检测服务存活状态", tags = {"探针"}) + @PostMapping("/probe/check") + public void probeCheck() { + + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiDictWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiDictWeb.java new file mode 100644 index 0000000..f93167e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiDictWeb.java @@ -0,0 +1,59 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Page; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.GiftDictListInput; +import com.sonic.frog.domain.output.AiDictOut; +import com.sonic.frog.domain.output.ChatModelDictOutput; +import com.sonic.frog.domain.output.GiftDictListOutput; +import com.sonic.frog.service.AiDictService; +import com.sonic.frog.service.ChatModelDictService; +import com.sonic.frog.service.GiftDictService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Web-Dict管理 + * + * @author code + */ +@RestController +@Slf4j +public class AiDictWeb { + + @Autowired + private AiDictService aiDictService; + @Autowired + private GiftDictService giftDictService; + @Autowired + private ChatModelDictService chatModelDictService; + + @ApiOperation(value = "获取AI字典信息", tags = {"Web-AI字典"}) + @PostMapping("/web/get-ai-dict") + @IgnoreAuth + public Result getAiDict(@ApiParam(hidden = true) Session session) { + return Result.success(aiDictService.getAiDict()); + } + + @ApiOperation(value = "获取礼物字典列表", tags = {"Web-AI字典"}) + @PostMapping("/web/gift/dict-list") + @IgnoreAuth + public Result> getGiftDictList(@Validated @RequestBody GiftDictListInput input, @ApiParam(hidden = true) Session session) { + return Result.success(giftDictService.getGiftDictList(input)); + } + + @ApiOperation(value = "获取对话模型字典列表", tags = {"Web-AI字典"}) + @PostMapping("/web/chat-model/dict-list") + @IgnoreAuth + public Result getChatModelDictList(@ApiParam(hidden = true) Session session) { + return Result.success(chatModelDictService.getChatModelDictList()); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserAlbumWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserAlbumWeb.java new file mode 100644 index 0000000..7148532 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserAlbumWeb.java @@ -0,0 +1,144 @@ +/* + * Copyright 2018 - 2050 zyp.All Rights Reserved. + * + */ +package com.sonic.frog.controller.web; + +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Page; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.IpAddressUtils; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.domain.entity.Liked; +import com.sonic.frog.domain.input.*; +import com.sonic.frog.domain.output.BatchAddAlbumOutput; +import com.sonic.frog.domain.output.ListAiAlbumOutput; +import com.sonic.frog.domain.output.ViewUnlockAlbumImgOutput; +import com.sonic.frog.limit.RequestLimit; +import com.sonic.frog.service.AiUserAlbumService; +import com.sonic.frog.service.UserCreateCountStatService; +import com.sonic.frog.utils.RedisKeyUtils; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Web-Ai用户相册 + * + * @author mzc + * @createTime 2019-06-26 19:47:58 + */ + +@RestController +public class AiUserAlbumWeb { + + @Autowired + private AppRuntime appRuntime; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private AiUserAlbumService aiUserAlbumService; + @Autowired + private UserCreateCountStatService userCreateCountStatService; + @Autowired + private RedisKeyUtils redisKeyUtils; + + + @RequestLimit(count = 1, loginCount = 10, message = "operate too frequently,please try again later (101) ") + @ApiOperation(value = "批量添加图片到相册", tags = {"WEB-Ai用户相册"}) + @PostMapping(value = "/web/ai-user/batch-add-album") + public Result batchAddAlbum(@Validated @RequestBody BatchAddAlbumInput addAlbum, @ApiParam(hidden = true) Session session, HttpServletRequest request) { + AtomicReference atomicReference = new AtomicReference(); + //操作人加锁,防止并发操作 + RedisLock redisLock = new RedisLock(appRuntime.buildPrefixKey(Liked.BizType.ALBUM_PIC.name(), "add", session.getUserId()), redisWrapper); + redisLock.tryAcquireRun(() -> { + //上传后返回主键id列表给前端 + List ids = aiUserAlbumService.batchAddAlbum(session.getUserId(), addAlbum); + //设置返回对象 + atomicReference.set(BatchAddAlbumOutput.builder().ids(ids).build()); + return true; + }); + return Result.success(atomicReference.get()); + } + + @RequestLimit(count = 30, loginCount = 40, message = "operate too frequently,please try again later (101) ") + @ApiOperation(value = "相册列表", tags = {"WEB-Ai用户相册"}) + @PostMapping(value = "/web/ai-user/album-list") + @IgnoreAuth + public Result> listAlbums(@Valid @RequestBody AlbumListInput input, @ApiParam(hidden = true) Session session, HttpServletRequest request) { + input.setIpAddress(IpAddressUtils.getIpAddress(request)); + Page listAlbums = aiUserAlbumService.listAlbums(input, session.getUserId()); + return Result.success(listAlbums); + } + + @ApiOperation(value = "删除图片", tags = {"WEB-Ai用户相册"}) + @PostMapping(value = "/web/ai-user/album-del") + public Result delAlbum(@Validated @RequestBody DelAlbumInput input, Session session) { + aiUserAlbumService.delAlbum(session.getUserId(), input.getAlbumId()); + return Result.success(); + } + + @RequestLimit(count = 30, loginCount = 40, message = "operate too frequently,please try again later (101) ") + @ApiOperation(value = "相册图片点赞、取消点赞", tags = {"WEB-Ai用户相册"}) + @PostMapping(value = "/web/album/like_or_cancel") + public Result likeOrCancel(@Validated @RequestBody LikeOrCancelPicInput input, @ApiParam(hidden = true) Session session, HttpServletRequest request) { + //操作人加锁,防止并发操作 + RedisLock redisLock = new RedisLock(appRuntime.buildPrefixKey(Liked.BizType.ALBUM_PIC.name(), "liked", session.getUserId(), input.getAlbumId()), redisWrapper); + redisLock.tryAcquireRun(() -> { + aiUserAlbumService.likeOrCancelPic(session.getUserId(), input); + return true; + }); + return Result.success(); + } + + @RequestLimit(count = 30, loginCount = 40, message = "operate too frequently,please try again later (101) ") + @ApiOperation(value = "设置默认图片", tags = {"WEB-Ai用户相册"}) + @PostMapping(value = "/web/ai-user/set-default-album") + public Result setDefaultAlbum(@Validated @RequestBody SetDefaultAlbumInput input, @ApiParam(hidden = true) Session session, HttpServletRequest request) { + aiUserAlbumService.setDefaultAlbum(session.getUserId(), input.getAiId(), input.getAlbumId()); + return Result.success(); + } + + @RequestLimit(count = 30, loginCount = 40, message = "operate too frequently,please try again later (101) ") + @ApiOperation(value = "设置相册解锁价格", tags = {"WEB-Ai用户相册"}) + @PostMapping(value = "/web/ai-user/set-album-unlock-price") + public Result setAlbumUnlockPrice(@Validated @RequestBody SetAlbumUnlockPriceInput input, @ApiParam(hidden = true) Session session, HttpServletRequest request) { + aiUserAlbumService.setAlbumUnlockPrice(session.getUserId(), input); + return Result.success(); + } + + @ApiOperation(value = "解锁加密图片", tags = {"WEB-Ai用户相册"}) + @PostMapping(value = "/web/ai-user/unlock-album-img") + public Result unlockAlbumImg(@Validated @RequestBody UnlockAlbumImgInput input, @ApiParam(hidden = true) Session session, HttpServletRequest request) { + //操作人加锁,防止并发操作 + String redisKey = redisKeyUtils.unlockAlbumImgLockKey(session.getUserId(), input.getAlbumId()); + RedisLock redisLock = new RedisLock(redisKey, redisWrapper); + ViewUnlockAlbumImgOutput output = redisLock.tryAcquireRun(() -> aiUserAlbumService.unlockAlbumImg(session.getUserId(), input)); + return Result.success(output); + } + + @ApiOperation(value = "加密图片解锁后访问", tags = {"WEB-Ai用户相册"}) + @PostMapping(value = "/web/ai-user/view-unlock-album-img") + public Result viewUnlockAlbumImg(@Validated @RequestBody ViewUnlockAlbumImgInput input, @ApiParam(hidden = true) Session session, HttpServletRequest request) { + input.setIpAddress(IpAddressUtils.getIpAddress(request)); + ViewUnlockAlbumImgOutput output = aiUserAlbumService.viewUnlockAlbumImg(session.getUserId(), input); + return Result.success(output); + } + + @ApiOperation(value = "获取用户相册创作次数", tags = {"Web-用户管理"}) + @PostMapping("/web/ai-user/get-user-create-count") + public Result getUserCreateCount(@ApiParam(hidden = true) Session session) { + return Result.success(userCreateCountStatService.getUserCreateCount(session.getUserId())); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserGiftWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserGiftWeb.java new file mode 100644 index 0000000..13bc5c6 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserGiftWeb.java @@ -0,0 +1,59 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Page; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.domain.input.AiUserGiftListInput; +import com.sonic.frog.domain.input.SendGiftInput; +import com.sonic.frog.domain.output.AiUserGiftListOutput; +import com.sonic.frog.service.AiUserGiftService; +import com.sonic.frog.utils.RedisKeyUtils; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Web-AI用户礼物 + * + * @author code + */ +@RestController +@Slf4j +public class AiUserGiftWeb { + + @Autowired + private AiUserGiftService aiUserGiftService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @IgnoreAuth + @ApiOperation(value = "获取用户获得的礼物列表", tags = {"Web-AI用户礼物"}) + @PostMapping("/web/ai-user-gift/list") + public Result> getAiUserGiftList(@Validated @RequestBody AiUserGiftListInput input, @ApiParam(hidden = true) Session session) { + return Result.success(aiUserGiftService.getAiUserGiftList(input)); + } + + @ApiOperation(value = "用户发送礼物", tags = {"Web-AI用户礼物"}) + @PostMapping("/web/ai-user-gift/send") + public Result sendGift(@Validated @RequestBody SendGiftInput input, @ApiParam(hidden = true) Session session) { + //redis键 + String sendGiftLockKey = redisKeyUtils.sendGiftLockKey(session.getUserId()); + //加锁处理 + RedisLock redisLock = new RedisLock(sendGiftLockKey, redisWrapper); + redisLock.tryAcquireRun(() -> { + aiUserGiftService.sendGift(session.getUserId(), input); + return true; + }); + return Result.success(); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserHeartbeatWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserHeartbeatWeb.java new file mode 100644 index 0000000..2d8e633 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserHeartbeatWeb.java @@ -0,0 +1,85 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Page; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.domain.input.*; +import com.sonic.frog.domain.output.*; +import com.sonic.frog.service.AiUserHeartbeatRankService; +import com.sonic.frog.service.AiUserHeartbeatRelationService; +import com.sonic.frog.utils.RedisKeyUtils; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Web-AI用户心动等级 + * + * @author code + */ +@RestController +@Slf4j +public class AiUserHeartbeatWeb { + + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private AiUserHeartbeatRankService aiUserHeartbeatRankService; + + @ApiOperation(value = "关系列表", tags = {"Web-AI用户心动等级"}) + @PostMapping("/web/ai-user/heartbeat-relation-list") + public Result> heartbeatRelationList(@Validated @RequestBody HeartbeatRelationListInput input, @ApiParam(hidden = true) Session session) { + return Result.success(aiUserHeartbeatRelationService.heartbeatRelationList(session.getUserId(), input)); + } + + @ApiOperation(value = "展示关系开关", tags = {"Web-AI用户心动等级"}) + @PostMapping("/web/ai-user/heartbeat-relation-switch") + public Result heartbeatRelationSwitch(@Validated @RequestBody HeartbeatRelationSwitchInput input, @ApiParam(hidden = true) Session session) { + aiUserHeartbeatRelationService.heartbeatRelationSwitch(session.getUserId(), input); + return Result.success(); + } + + @ApiOperation(value = "获取心动等级", tags = {"Web-AI用户心动等级"}) + @PostMapping("/web/ai-user/heartbeat-level") + public Result getAiUserHeartbeatLevel(@Validated @RequestBody HeartbeatLevelInput input, @ApiParam(hidden = true) Session session) { + return Result.success(aiUserHeartbeatRelationService.getAiUserHeartbeatLevel(session.getUserId(), input.getAiId())); + } + + @ApiOperation(value = "购买已扣减的心动值", tags = {"Web-AI用户心动等级"}) + @PostMapping("/web/ai-user/buy-heartbeat-val") + public Result buyHeartbeatVal(@Validated @RequestBody BuyHeartbeatValInput input, @ApiParam(hidden = true) Session session) { + //redis键 + String buyHeartbeatValLockKey = redisKeyUtils.buyHeartbeatValLockKey(session.getUserId()); + //加锁处理 + RedisLock redisLock = new RedisLock(buyHeartbeatValLockKey, redisWrapper); + redisLock.tryAcquireRun(() -> { + aiUserHeartbeatRelationService.buyHeartbeatVal(session.getUserId(), input); + return true; + }); + return Result.success(); + } + + @ApiOperation(value = "24小时未聊天扣除心动值(登录后调一次)", tags = {"Web-AI用户心动等级"}) + @PostMapping("/web/ai-user/without-chat-subtract-heartbeat-val") + public Result withoutChatSubtractHeartbeatVal(@ApiParam(hidden = true) Session session) { +// aiUserHeartbeatRelationService.withoutChatSubtractHeartbeatVal(session.getUserId()); + return Result.success(); + } + + @ApiOperation(value = "获取当前用户的心动值排名", tags = {"Web-AI用户心动等级"}) + @PostMapping("/web/ai-user/heartbeat-rank") + public Result heartbeatRank(@ApiParam(hidden = true) Session session) { + return Result.success(aiUserHeartbeatRankService.getCurrentUserRank(session.getUserId())); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserMeetWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserMeetWeb.java new file mode 100644 index 0000000..b394c1e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserMeetWeb.java @@ -0,0 +1,98 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.domain.input.MeetInput; +import com.sonic.frog.domain.input.MeetSdInput; +import com.sonic.frog.domain.input.MeetUnlockInput; +import com.sonic.frog.domain.output.AiAlbumDetailOutput; +import com.sonic.frog.domain.output.AiUserBaseOutput; +import com.sonic.frog.domain.output.MeetSdOutput; +import com.sonic.frog.service.AiUserAlbumService; +import com.sonic.frog.service.AiUserSearchService; +import com.sonic.frog.service.MeetService; +import com.sonic.frog.service.UserAiMeetService; +import com.sonic.frog.utils.RedisKeyUtils; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.security.InvalidKeyException; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Web-Meet遇见 + * + * @author code + */ +@RestController +@Slf4j +public class AiUserMeetWeb { + + @Autowired + private UserAiMeetService userAiMeetService; + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private AiUserAlbumService aiUserAlbumService; + @Autowired + private MeetService meetService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @IgnoreAuth + @ApiOperation(value = "上报滑动次数", tags = {"Web-Meet遇见"}) + @PostMapping("/web/meet/sd") + public Result meetSd(@Validated @RequestBody MeetSdInput input, @ApiParam(hidden = true) Session session) { + AtomicReference meetSdOutput = new AtomicReference(); + //加锁处理 + RedisLock redisLock = new RedisLock(redisKeyUtils.meetSdLockKey(session.getUserId()), redisWrapper); + redisLock.tryAcquireRun(() -> { + meetSdOutput.set(userAiMeetService.meetSd(session.getUserId(), input.getAiId(), input.getLk()));; + return true; + }); + return Result.success(meetSdOutput.get()); + } + + @IgnoreAuth + @ApiOperation(value = "上报meet绑定", tags = {"Web-Meet遇见"}) + @PostMapping("/web/meet/bd") + public Result meetBd(@Validated @RequestBody MeetInput input, @ApiParam(hidden = true) Session session) { + boolean bl = userAiMeetService.meetBd(session.getUserId(), input.getAiId()); + if(bl) { + return Result.success(aiUserSearchService.getAiUserBaseInfo(session.getUserId(), input.getAiId())); + } + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "上报meet爱慕者推荐并返回模糊相册ID", tags = {"Web-Meet遇见"}) + @PostMapping("/web/meet/rc") + public Result meetRc(@ApiParam(hidden = true) Session session) throws InvalidKeyException { + //随机推荐AI + Long albumId = userAiMeetService.meetRc(session.getUserId()); + if(albumId == null) { + return Result.success(); + } + return Result.success(aiUserAlbumService.rcDetail(albumId)); + } + + @IgnoreAuth + @ApiOperation(value = "meet爱慕者解锁", tags = {"Web-Meet遇见"}) + @PostMapping("/web/meet/unlock") + public Result meetUnLock(@RequestBody MeetUnlockInput input, @ApiParam(hidden = true) Session session) { + //购买,产生流水 + meetService.meetUnLock(session.getUserId(), input); + return Result.success(); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserSearchWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserSearchWeb.java new file mode 100644 index 0000000..6578301 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserSearchWeb.java @@ -0,0 +1,76 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.*; +import com.sonic.frog.domain.output.*; +import com.sonic.frog.limit.RequestLimit; +import com.sonic.frog.service.AiUserSearchService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * Web-AI用户查询 + * + * @author code + */ +@RestController +@Slf4j +public class AiUserSearchWeb { + + @Autowired + private AiUserSearchService aiUserSearchService; + + @IgnoreAuth + @ApiOperation(value = "获取AI基础信息", tags = {"Web-AI用户查询"}) + @PostMapping("/web/ai-user-search/base-info") + public Result getAiUserBaseInfo(@Validated @RequestBody AiUserBaseInfoInput input, @ApiParam(hidden = true) Session session) { + return Result.success(aiUserSearchService.getAiUserBaseInfo(session.getUserId(), input.getAiId())); + } + + @ApiOperation(value = "获取用户自己的AI列表", tags = {"Web-AI用户查询"}) + @PostMapping("/web/ai-user-search/base-list") + public Result> baseAiList(@ApiParam(hidden = true) Session session) { + return Result.success(aiUserSearchService.getBaseUserAiList(session.getUserId())); + } + + @RequestLimit(count = 20, message = "operate too frequently,please try again 1min later") + @ApiOperation(value = "目标用户的AI列表", tags = {"Web-AI用户查询"}) + @PostMapping("/web/ai-user-search/target-list") + public Result> targetAiList(@RequestBody @Validated UserIdInput input) { + return Result.success(aiUserSearchService.getTargetUserAiList(input.getUserId())); + } + + @IgnoreAuth + @ApiOperation(value = "获取AI用户H5信息", tags = {"Web-AI用户查询"}) + @PostMapping("/web/ai-user-search/h5-info") + public Result getAiUserH5Info(@Validated @RequestBody AiUserH5InfoInput input, @ApiParam(hidden = true) Session session) { + return Result.success(aiUserSearchService.getAiUserH5Info(input.getAiId())); + } + + @IgnoreAuth + @ApiOperation(value = "获取AI用户seo基础信息", tags = {"Web-AI用户查询"}) + @PostMapping("/web//so/base-nfo") + public Result getAiUserSeoBaseInfo(@Validated @RequestBody AiUserSeoBaseInfoInput input, @ApiParam(hidden = true) Session session) { + return Result.success(aiUserSearchService.getAiUserSeoBaseInfo(input.getAiId())); + } + + @IgnoreAuth + @ApiOperation(value = "获取im聊天AI基础信息", tags = {"Web-AI用户查询"}) + @PostMapping("/web/ai-user-search/im-base-info") + public Result getAiUserImBaseInfo(@Validated @RequestBody AiUserImBaseInfoInput input, @ApiParam(hidden = true) Session session) { + return Result.success(aiUserSearchService.getAiUserImBaseInfo(input.getAiId(), session.getUserId())); + } + + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserSetWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserSetWeb.java new file mode 100644 index 0000000..7b8b33d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserSetWeb.java @@ -0,0 +1,73 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.domain.input.CreateEditAiUserInput; +import com.sonic.frog.domain.input.DelAiUserInput; +import com.sonic.frog.domain.input.EditAiHeadImgInput; +import com.sonic.frog.domain.input.GetAiUserInfoInput; +import com.sonic.frog.domain.output.AiUserInfoOutput; +import com.sonic.frog.domain.output.CreateEditAiUserOutput; +import com.sonic.frog.service.AiUserSetService; +import com.sonic.frog.utils.RedisKeyUtils; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + * Web-AI用户设置 + * + * @author: mzc + * @date: 2025-07-11 11:43 + **/ +@RestController +@Slf4j +public class AiUserSetWeb { + + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private AiUserSetService aiUserSetService; + + @ApiOperation(value = "创建或编辑AI", tags = {"Web-AI用户设置"}) + @PostMapping("/web/ai-user/create-edit") + public Result createEditAiUser(@Valid @Validated @RequestBody CreateEditAiUserInput input, @ApiParam(hidden = true) Session session) { + //redis键 + String aiUserCreateEditLockKey = redisKeyUtils.aiUserCreateEditLockKey(session.getUserId()); + //加锁处理 + RedisLock redisLock = new RedisLock(aiUserCreateEditLockKey, redisWrapper); + CreateEditAiUserOutput output = redisLock.tryAcquireRun(() -> { + return aiUserSetService.createEditAiUser(input, session.getUserId()); + }); + return Result.success(output); + } + + @ApiOperation(value = "删除AI用户", tags = {"Web-AI用户设置"}) + @PostMapping("/web/ai-user/del") + public Result delAiUser(@Valid @RequestBody DelAiUserInput input, @ApiParam(hidden = true) Session session) { + aiUserSetService.delAiUser(input.getAiId(), session.getUserId()); + return Result.success(); + } + + @ApiOperation(value = "获取AI信息", tags = {"Web-AI用户设置"}) + @PostMapping("/web/ai-user/get-my-ai-user/info") + public Result getMyAiUserInfo(@Valid @RequestBody GetAiUserInfoInput input, @ApiParam(hidden = true) Session session) { + return Result.success(aiUserSetService.getMyAiUserInfo(input.getAiId(), session.getUserId())); + } + + @ApiOperation(value = "修改AI头像", tags = {"WEB-AI用户设置"}) + @RequestMapping(value = "/web/ai-user/edit-head-img", produces = {"application/json"}, method = RequestMethod.POST) + public Result editAiHeadImg(@Valid @RequestBody EditAiHeadImgInput input,@ApiParam(hidden = true) Session session) { + aiUserSetService.editAiHeadImg(session.getUserId(), input); + return Result.success(); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserStatWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserStatWeb.java new file mode 100644 index 0000000..e3cb93f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/AiUserStatWeb.java @@ -0,0 +1,37 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.AiUserStatInput; +import com.sonic.frog.domain.output.AiUserStatOutput; +import com.sonic.frog.service.AiUserStatService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Web-AI角色统计信息 + * + * @author code + */ +@RestController +@Slf4j +public class AiUserStatWeb { + + @Autowired + private AiUserStatService aiUserStatService; + + @IgnoreAuth + @ApiOperation(value = "获取Ai用户统计信息", tags = {"Web-AI角统计"}) + @PostMapping("/web/ai-user/stat") + public Result getAiUserStatInfo(@Validated @RequestBody AiUserStatInput input, @ApiParam(hidden = true) Session session) { + return Result.success(aiUserStatService.getAiUserStatByAiId(input.getAiId(), session.getUserId())); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ChatBackgroundWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ChatBackgroundWeb.java new file mode 100644 index 0000000..9dd39fa --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ChatBackgroundWeb.java @@ -0,0 +1,79 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.domain.input.BatchAddBackgroundInput; +import com.sonic.frog.domain.input.ChatSetInput; +import com.sonic.frog.domain.input.DelBackgroundInput; +import com.sonic.frog.domain.input.SetBackgroundInput; +import com.sonic.frog.domain.output.BackgroundImgListOutput; +import com.sonic.frog.domain.output.BatchAddBackgroundOutput; +import com.sonic.frog.service.ChatUserBackgroundService; +import com.sonic.frog.utils.RedisKeyUtils; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Web-聊天背景 + * + * @author code + */ +@RestController +@Slf4j +public class ChatBackgroundWeb { + + @Autowired + private ChatUserBackgroundService chatUserBackgroundService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @ApiOperation(value = "批量添加背景", tags = {"Web-聊天背景"}) + @PostMapping("/web/chat-background/batch-add") + public Result> batchAddBackground(@Validated @RequestBody BatchAddBackgroundInput input, @ApiParam(hidden = true) Session session) { + AtomicReference atomicReference = new AtomicReference(); + //操作人加锁,防止并发操作 + String key = redisKeyUtils.backgroundAddLockKey(session.getUserId()); + RedisLock redisLock = new RedisLock(key, redisWrapper); + redisLock.tryAcquireRun(() -> { + //上传后返回主键id列表给前端 + List ids = chatUserBackgroundService.batchAddBackground(session.getUserId(), input); + //设置返回对象 + atomicReference.set(BatchAddBackgroundOutput.builder().ids(ids).build()); + return true; + }); + return Result.success(atomicReference.get()); + } + + @ApiOperation(value = "获取聊天背景列表", tags = {"Web-聊天背景"}) + @PostMapping("/web/chat-background/list") + public Result getBackgroundImgList(@Validated @RequestBody ChatSetInput input, @ApiParam(hidden = true) Session session) { + return Result.success(chatUserBackgroundService.getBackgroundImgList(session.getUserId(), input.getAiId())); + } + + @ApiOperation(value = "设置聊天背景", tags = {"Web-聊天背景"}) + @PostMapping("/web/chat-background/set-background") + public Result setBackground(@Validated @RequestBody SetBackgroundInput input, @ApiParam(hidden = true) Session session) { + chatUserBackgroundService.setBackground(session.getUserId(), input); + return Result.success(); + } + + @ApiOperation(value = "删除聊天背景", tags = {"Web-聊天背景"}) + @PostMapping("/web/chat-background/del") + public Result delBackground(@Validated @RequestBody DelBackgroundInput input, @ApiParam(hidden = true) Session session) { + chatUserBackgroundService.delBackground(session.getUserId(), input.getBackgroundId()); + return Result.success(); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ChatSetWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ChatSetWeb.java new file mode 100644 index 0000000..f5b3179 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ChatSetWeb.java @@ -0,0 +1,74 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.*; +import com.sonic.frog.domain.output.ChatBubbleListOutput; +import com.sonic.frog.domain.output.ChatSetOutput; +import com.sonic.frog.service.ChatSetService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * Web-聊天设置 + * + * @author code + */ +@RestController +@Slf4j +public class ChatSetWeb { + + @Autowired + private ChatSetService chatSetService; + + @ApiOperation(value = "获取我的聊天设置", tags = {"Web-聊天设置"}) + @PostMapping("/web/chat-set/get-my") + public Result getMyChatSet(@Validated @RequestBody ChatSetInput input, @ApiParam(hidden = true) Session session) { + return Result.success(chatSetService.getMyChatSet(session.getUserId(), input.getAiId())); + } + + @ApiOperation(value = "设置我的聊天设定", tags = {"Web-聊天设置"}) + @PostMapping("/web/chat-set/set") + public Result setMyChatSetting(@Validated @RequestBody SetMyChatSettingInput input, @ApiParam(hidden = true) Session session) { + chatSetService.setMyChatSetting(session.getUserId(), input); + return Result.success(); + } + + @ApiOperation(value = "设置对话模型", tags = {"Web-聊天设置"}) + @PostMapping("/web/chat-set/set-chat-model") + public Result setChatModel(@Validated @RequestBody SetChatModelInput input, @ApiParam(hidden = true) Session session) { + chatSetService.setChatModel(session.getUserId(), input); + return Result.success(); + } + + @ApiOperation(value = "设置聊天气泡字典列表", tags = {"Web-聊天设置"}) + @PostMapping("/web/chat-set/get-chat-bubble-list") + public Result> getChatBubbleList(@Validated @RequestBody ChatBubbleListInput input, @ApiParam(hidden = true) Session session) { + List outputList = chatSetService.getChatBubbleList(session.getUserId(), input); + return Result.success(outputList); + } + + @ApiOperation(value = "设置聊天气泡", tags = {"Web-聊天设置"}) + @PostMapping("/web/chat-set/set-chat-bubble") + public Result setChatBubble(@Validated @RequestBody SetChatBubbleInput input, @ApiParam(hidden = true) Session session) { + chatSetService.setChatBubble(session.getUserId(), input); + return Result.success(); + } + + @ApiOperation(value = "设备是否自动播放语音", tags = {"Web-聊天设置"}) + @PostMapping("/web/chat-set/auto-play-voice") + public Result setIsAutoPlayVoice(@Validated @RequestBody SetIsAutoPlayVoiceInput input, @ApiParam(hidden = true) Session session) { + chatSetService.setIsAutoPlayVoice(session.getUserId(), input); + return Result.success(); + } + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ChatWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ChatWeb.java new file mode 100644 index 0000000..661b377 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ChatWeb.java @@ -0,0 +1,39 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.*; +import com.sonic.frog.domain.output.ChatBubbleListOutput; +import com.sonic.frog.domain.output.ChatSetOutput; +import com.sonic.frog.service.ChatService; +import com.sonic.frog.service.ChatSetService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * Web-聊天 + * + * @author code + */ +@RestController +@Slf4j +public class ChatWeb { + + @Autowired + private ChatService chatService; + + @ApiOperation(value = "发送开场白消息", tags = {"Web-聊天"}) + @PostMapping("/web/chat/send-dialogue-prologue-message") + public Result sendDialoguePrologueMessage(@Validated @RequestBody SendDialoguePrologueMessageInput input, @ApiParam(hidden = true) Session session) { + chatService.sendDialoguePrologueMessage(input.getAiId(),session.getUserId()); + return Result.success(); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ExploreWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ExploreWeb.java new file mode 100644 index 0000000..68663ad --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/ExploreWeb.java @@ -0,0 +1,37 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.output.ExploreInfoOutput; +import com.sonic.frog.service.ExploreService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * Web-发现 + * @author code + */ +@Slf4j +@Validated +@RestController +public class ExploreWeb { + + @Autowired + private ExploreService exploreService; + + @IgnoreAuth + @ApiOperation(value = "发现", tags = {"Web-发现"}) + @PostMapping("/web/explore/info") + public Result exploreInfo(@ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + return Result.success(exploreService.exploreInfo(session.getUserId())); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/HomeWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/HomeWeb.java new file mode 100644 index 0000000..2493f4b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/HomeWeb.java @@ -0,0 +1,85 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.input.AiUserImBaseInfoInput; +import com.sonic.frog.domain.input.ClassificationListInput; +import com.sonic.frog.domain.input.HomeRecommendInput; +import com.sonic.frog.domain.output.AiCarouselListOutput; +import com.sonic.frog.domain.output.ClassificationListOutput; +import com.sonic.frog.domain.output.HomeRecommendOutput; +import com.sonic.frog.domain.output.HomeRecommendV2Output; +import com.sonic.frog.service.AdvertiseService; +import com.sonic.frog.service.HomeClassificationService; +import com.sonic.frog.service.HomeRecommendService; +import com.sonic.frog.service.HomeRecommendV2Service; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +/** + * Web-首页推荐 + * + * @author code + */ +@RestController +@Slf4j +public class HomeWeb { + + @Autowired + private HomeRecommendService homeRecommendService; + @Autowired + private HomeClassificationService homeClassificationService; + @Autowired + private AdvertiseService advertiseService; + @Autowired + private HomeRecommendV2Service homeRecommendV2Service; + + @IgnoreAuth + @ApiOperation(value = "首页推荐卡片列表", tags = {"Web-首页推荐"}) + @PostMapping("/web/home/rm-list") + public Result> recommendList(@Validated @RequestBody HomeRecommendInput input, @ApiParam(hidden = true) Session session) { + input.setRandom(true); + return Result.success(homeRecommendService.recommendList(session.getUserId(), input)); + } + + @IgnoreAuth + @ApiOperation(value = "获取AI基础信息", tags = {"Web-首页推荐"}) + @PostMapping("/web/home/meet-detail") + public Result getAiMeetDetail(@Validated @RequestBody AiUserImBaseInfoInput input, @ApiParam(hidden = true) Session session) { + return Result.success(homeRecommendService.getAiMeetDetail(session.getUserId(), input.getAiId())); + } + + @IgnoreAuth + @ApiOperation(value = "首页分类列表", tags = {"Web-首页推荐"}) + @PostMapping("/web/home/classification-list") + public Result> classificationList(@Validated @RequestBody ClassificationListInput input, @ApiParam(hidden = true) Session session) { + return Result.success(homeClassificationService.classificationList(session.getUserId(), input)); + } + + @IgnoreAuth + @ApiOperation(value = "首页AI轮播列表", tags = {"Web-首页推荐"}) + @PostMapping("/web/home/ai-carousel-list") + public Result> getAiCarouselList(@ApiParam(hidden = true) Session session) { + return Result.success(advertiseService.getAiCarouselList(session.getUserId())); + } + + @IgnoreAuth + @ApiOperation(value = "首页聚合推荐", tags = {"Web-首页推荐"}) + @PostMapping("/web/home/agg-recommend") + public Result aggRecommend() { + log.info("===> LocalDateTime : {}, Date : {}", LocalDateTime.now(), new Date()); + return Result.success(homeRecommendV2Service.recommendList()); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/LikedWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/LikedWeb.java new file mode 100644 index 0000000..a99c5ef --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/LikedWeb.java @@ -0,0 +1,56 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.domain.entity.Liked; +import com.sonic.frog.domain.input.AiUserLikeOrCancelInput; +import com.sonic.frog.domain.input.AiUserStatInput; +import com.sonic.frog.domain.input.LikeOrCancelPicInput; +import com.sonic.frog.domain.output.AiUserStatOutput; +import com.sonic.frog.limit.RequestLimit; +import com.sonic.frog.service.AiUserStatService; +import com.sonic.frog.service.LikedService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * Web-点赞 + * + * @author code + */ +@RestController +@Slf4j +public class LikedWeb { + + @Autowired + private AppRuntime appRuntime; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private LikedService likedService; + + @RequestLimit(count = 30, loginCount = 40, message = "operate too frequently,please try again later (101) ") + @ApiOperation(value = "ai用户点赞、取消点赞", tags = {"WEB-点赞"}) + @PostMapping(value = "/web/ai-user/like-or-cancel") + public Result likeOrCancel(@Validated @RequestBody AiUserLikeOrCancelInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + //操作人加锁,防止并发操作 + RedisLock redisLock = new RedisLock(appRuntime.buildPrefixKey(Liked.BizType.AI.name(), "liked", session.getUserId(), input.getAiId()), redisWrapper); + redisLock.tryAcquireRun(() -> { + likedService.aiUserLikeOrCancel(session.getUserId(), input); + return true; + }); + return Result.success(); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/LoginWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/LoginWeb.java new file mode 100644 index 0000000..95b4d04 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/LoginWeb.java @@ -0,0 +1,57 @@ +package com.sonic.frog.controller.web; + +import com.sonic.bear.lib.client.UserLoginClient; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.IpAddressUtils; +import com.sonic.frog.domain.input.ThirdLoginOrRegisterInput; +import com.sonic.frog.domain.output.ThirdLoginOrRegisterOutput; +import com.sonic.frog.limit.RequestLimit; +import com.sonic.frog.service.ThirdLoginOrRegisterService; +import com.sonic.frog.utils.HttpHeaderUtils; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; + +/** + * Web-三方账号 + * @author code + */ +@RestController +@Slf4j +public class LoginWeb { + + @Autowired + private ThirdLoginOrRegisterService thirdLoginOrRegisterService; + @Autowired + private UserLoginClient userLoginClient; + + @IgnoreAuth + @RequestLimit(count=50, time=300000, message = "operate too frequently,please try again 5min later (101) ") + @ApiOperation(value = "三方账号注册登录", tags = {"Web-三方账号"}) + @PostMapping("/web/third/login") + public Result loginOrRegister(@Valid @RequestBody ThirdLoginOrRegisterInput input, @ApiParam(hidden = true) HttpServletRequest request) { + input.setIp(IpAddressUtils.getIpAddress(request)); + input.setUserAgent(HttpHeaderUtils.getUserAgent(request)); + //验证用户的账号是要进行密码登录还是注册 + ThirdLoginOrRegisterOutput output = thirdLoginOrRegisterService.loginOrRegister(input); + return Result.success(output); + } + + + @ApiOperation(value = "退出登录", tags = {"Web-三方账号"}) + @PostMapping("/web/user/logout") + public Result logout(@ApiParam(hidden = true) Session session) { + userLoginClient.logout(session.getToken()); + return Result.success(); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/PayWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/PayWeb.java new file mode 100644 index 0000000..f22def5 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/PayWeb.java @@ -0,0 +1,67 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.domain.input.*; +import com.sonic.frog.domain.output.AiUserInfoOutput; +import com.sonic.frog.domain.output.CreateEditAiUserOutput; +import com.sonic.frog.domain.output.ViewUnlockAlbumImgOutput; +import com.sonic.frog.service.AiUserSetService; +import com.sonic.frog.service.PayService; +import com.sonic.frog.utils.RedisKeyUtils; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +/** + * Web-业务支付 + * + * @author: mzc + * @date: 2025-07-11 11:43 + **/ +@RestController +@Slf4j +public class PayWeb { + + @Autowired + private PayService payService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @ApiOperation(value = "创建Ai形象-购买创作次数", tags = {"Web-业务支付"}) + @PostMapping("/web/ai/buy-create-image-count") + public Result buyCreateImageCount(@Valid @RequestBody BuyCreateImageCountInput input, @ApiParam(hidden = true) Session session) { + //redis键 + String buyCreateImageCountLockKey = redisKeyUtils.buyCreateImageCountLockKey(session.getUserId()); + //加锁处理 + RedisLock redisLock = new RedisLock(buyCreateImageCountLockKey, redisWrapper); + redisLock.tryAcquireRun(() -> { + payService.buyCreateImageCount(input, session.getUserId()); + return true; + }); + return Result.success(); + } + + @ApiOperation(value = "解锁爱慕者", tags = {"Web-业务支付"}) + @PostMapping("/web/ai/unlock-like-you") + public Result unlockLikeYou(@Valid @RequestBody UnlockLikeYouInput input, @ApiParam(hidden = true) Session session) { + //redis键 + String unlockLikeYouLockKey = redisKeyUtils.unlockLikeYouLockKey(session.getUserId()); + //加锁处理 + RedisLock redisLock = new RedisLock(unlockLikeYouLockKey, redisWrapper); + ViewUnlockAlbumImgOutput output = redisLock.tryAcquireRun(() -> { + return payService.unlockLikeYou(input, session.getUserId()); + }); + return Result.success(output); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/RankWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/RankWeb.java new file mode 100644 index 0000000..2c65e2a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/RankWeb.java @@ -0,0 +1,57 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.output.AiChatRankOutput; +import com.sonic.frog.domain.output.AiGiftRankOutput; +import com.sonic.frog.domain.output.AiHeartbeatRankOutput; +import com.sonic.frog.service.RankService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * Web-榜单 + * + * @author code + */ +@Slf4j +@Validated +@RestController +public class RankWeb { + + @Autowired + private RankService rankService; + + @IgnoreAuth + @ApiOperation(value = "热聊榜", tags = {"Web-榜单"}) + @PostMapping("/web/rank/chat") + public Result> chatRank(@ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + return Result.success(rankService.chatRank(session.getUserId(), 100)); + } + + + @IgnoreAuth + @ApiOperation(value = "AI的总心动值榜单", tags = {"Web-榜单"}) + @PostMapping("/web/rank/heartbeat") + public Result> aiHeartbeatRank(@ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + return Result.success(rankService.aiHeartbeatRank(session.getUserId(), 100)); + } + + + @IgnoreAuth + @ApiOperation(value = "礼物榜单", tags = {"Web-榜单"}) + @PostMapping("/web/rank/gift") + public Result> giftRank(@ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + return Result.success(rankService.giftRank(session.getUserId(), 100)); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/SignInWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/SignInWeb.java new file mode 100644 index 0000000..ac19768 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/SignInWeb.java @@ -0,0 +1,44 @@ +package com.sonic.frog.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.frog.domain.output.SignInRoundOutput; +import com.sonic.frog.service.SignInRecordService; +import com.sonic.frog.service.SignInStatSearchService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * Web-签到 + * @author code + */ +@Slf4j +@Validated +@RestController +public class SignInWeb { + + @Autowired + private SignInRecordService signInRecordService; + @Autowired + private SignInStatSearchService signInStatSearchService; + + @ApiOperation(value = "签到", tags = {"Web-签到"}) + @PostMapping("/web/si/asi") + public Result signIn(@ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + return Result.success(signInRecordService.signIn(session.getUserId())); + } + + @ApiOperation(value = "七天签到列表", tags = {"Web-签到"}) + @PostMapping("/web/si/list") + public Result signInList(@ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + return Result.success(signInStatSearchService.signInList(session.getUserId())); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/UserWeb.java b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/UserWeb.java new file mode 100644 index 0000000..863ca8e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/controller/web/UserWeb.java @@ -0,0 +1,87 @@ +package com.sonic.frog.controller.web; + +import com.sonic.bear.lib.client.UserNicknamePoolClient; +import com.sonic.bear.lib.input.CompleteUserInfoInput; +import com.sonic.bear.lib.input.EditUserInfoInput; +import com.sonic.bear.lib.input.UserNicknameExistCheckInput; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.cow.lib.client.NsfwCheckClient; +import com.sonic.frog.domain.output.UserBaseInfoOutput; +import com.sonic.frog.domain.output.UserCreateCountOutput; +import com.sonic.frog.service.UserCreateCountStatService; +import com.sonic.frog.service.UserService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +/** + * Web-用户管理 + * + * @author code + */ +@RestController +@Slf4j +public class UserWeb { + + @Autowired + private UserService userService; + @Autowired + private UserNicknamePoolClient userNicknamePoolClient; + @Autowired + private UserCreateCountStatService userCreateCountStatService; + @Autowired + private NsfwCheckClient nsfwCheckClient; + + @ApiOperation(value = "获取用户自己的基础信息", tags = {"Web-用户管理"}) + @PostMapping("/web/user/base-info") + public Result baseInfo(@ApiParam(hidden = true) Session session) { + return Result.success(userService.getbaseUserInfo(session.getUserId())); + } + + @ApiOperation(value = "完善用户基础信息", tags = {"Web-用户管理"}) + @PostMapping("/web/user/complete-user-info") + public Result completeUserInfo(@Valid @RequestBody CompleteUserInfoInput input, @ApiParam(hidden = true) Session session) { + input.setUserId(session.getUserId()); + userService.completeUserInfo(input); + return Result.success(); + } + + @ApiOperation(value = "修改用户基础信息", tags = {"Web-用户管理"}) + @PostMapping("/web/user/edit-user-info") + public Result editUserInfo(@Valid @RequestBody EditUserInfoInput input, @ApiParam(hidden = true) Session session) { + input.setUserId(session.getUserId()); + userService.editUserInfo(input); + return Result.success(); + } + + @ApiOperation(value = "删除账号", tags = {"Web-用户管理"}) + @PostMapping("/web/user/del") + public Result delAccount(@ApiParam(hidden = true) Session session) { + userService.delAccount(session.getUserId()); + return Result.success(); + } + + @ApiOperation(value = "校验昵称是否已经存在", tags = {"Web-用户管理"}) + @PostMapping("/web/user/nickname-check") + public Result nicknameCheck(@Valid @RequestBody UserNicknameExistCheckInput input, @ApiParam(hidden = true) Session session) { + //敏感词校验 + nsfwCheckClient.checkContent(input.getNickname()); + return Result.success(userNicknamePoolClient.userNicknameExistCheck(input.getExUserId() == null ? session.getUserId() : input.getExUserId(), input.getNickname())); + } + + + @ApiOperation(value = "获取用户相册创作次数", tags = {"Web-用户管理"}) + @PostMapping("/web/user/get-user-create-count") + public Result getUserCreateCount(@ApiParam(hidden = true) Session session) { + return Result.success(userCreateCountStatService.getUserCreateCount(session.getUserId())); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/AdvertiseDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AdvertiseDao.java new file mode 100644 index 0000000..c109584 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AdvertiseDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.Advertise; +import org.apache.ibatis.annotations.Mapper; + +/** + * 广告管理 Dao + */ +@Mapper +public interface AdvertiseDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiDictDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiDictDao.java new file mode 100644 index 0000000..d1b94b3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiDictDao.java @@ -0,0 +1,15 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.AiDict; +/** + *

+ * AI角色,性格,标签,形象风格字典表 Mapper 接口 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +public interface AiDictDao extends BaseMapper { +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserAlbumDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserAlbumDao.java new file mode 100644 index 0000000..3ca1b46 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserAlbumDao.java @@ -0,0 +1,51 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.sonic.frog.domain.bo.AIUserAlbumGroupBo; +import com.sonic.frog.domain.entity.AiUserAlbum; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * AI用户相册数据访问接口 + */ +@Mapper +public interface AiUserAlbumDao extends BaseMapper { + /** + * 增加点赞数 + * + * @param id + * @param count + */ + void incrementLikedCount(@Param("id") Long id, @Param("count") int count); + + /** + * 分页查询AI用户相册 + * + * @param objectPage + * @param aiId + * @param unlockAlbumIds + * @return + */ + IPage listAlbumsPage(Page objectPage, @Param("aiId") Long aiId, @Param("unlockAlbumIds") List unlockAlbumIds); + + /** + * 根据用户查询数据并分组 + * @param aiIdList + * @return + */ + List listAlbumsGroup(@Param("aiIdList") List aiIdList); + + /** + * 获取待解锁图片 + * @param userId + * @param aiId + * @return + */ + List getRandomLockImage(@Param("userId") Long userId, @Param("aiId") Long aiId); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserAlbumUnlockDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserAlbumUnlockDao.java new file mode 100644 index 0000000..d89512a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserAlbumUnlockDao.java @@ -0,0 +1,15 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.AiUserAlbum; +import com.sonic.frog.domain.entity.AiUserAlbumUnlock; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * AI用户相册解锁数据访问接口 + */ +@Mapper +public interface AiUserAlbumUnlockDao extends BaseMapper { + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserDao.java new file mode 100644 index 0000000..7db9b92 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserDao.java @@ -0,0 +1,52 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.input.AgeRange; +import com.sonic.frog.domain.input.ClassificationListInput; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * AI用户表数据库访问层 + */ +@Mapper +public interface AiUserDao extends BaseMapper { + + + /** + * 随机获取AI列表 + * + * @param currentUserId + * @param exAiIdList + * @param count + * @return + */ + List randomGetAiUser(@Param("currentUserId") Long currentUserId, + @Param("exAiIdList") List exAiIdList, + @Param("roleCodeList") List roleCodeList, + @Param("isRandom") boolean isRandom, + @Param("count") Integer count, + @Param("sexList") List sexList, + @Param("ageRangeList") List ageRangeList + ); + + /** + * 根据被喜欢数范围获取AI列表 + * + * @param input + * @param minLiked + * @param maxLiked + * @param limit + * @return + */ + List queryAiIdsByLikedRange(@Param("input") ClassificationListInput input, + @Param("minLiked") int minLiked, + @Param("maxLiked") int maxLiked, + @Param("limit") int limit); + + List homeRecommend(@Param("lastChatTimeIsNull") Boolean lastChatTimeIsNull, @Param("count") Integer count); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserExtDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserExtDao.java new file mode 100644 index 0000000..e2c619d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserExtDao.java @@ -0,0 +1,11 @@ +package com.sonic.frog.dao; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.AiUserExt; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI用户扩展表数据库访问层 + */ +@Mapper +public interface AiUserExtDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserGiftDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserGiftDao.java new file mode 100644 index 0000000..2ce0ad6 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserGiftDao.java @@ -0,0 +1,35 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.sonic.frog.domain.entity.AiUserGift; +import com.sonic.frog.domain.output.AiUserGiftListOutput; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import javax.validation.constraints.NotNull; + +/** + * AI用户收到礼物数据访问接口 + */ +@Mapper +public interface AiUserGiftDao extends BaseMapper { + /** + * 分页获取用户获得的礼物列表 + * + * @param page + * @param aiId + * @return + */ + IPage getAiUserGiftList(Page page, @Param("aiId") Long aiId); + + /** + * 增加礼物数量 + * + * @param aiId + * @param giftId + * @param num + */ + void increaseGiftNum(@Param("aiId") Long aiId, @Param("giftId") Long giftId, @Param("num") Integer num); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserHeartbeatRankDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserHeartbeatRankDao.java new file mode 100644 index 0000000..6abc6a1 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserHeartbeatRankDao.java @@ -0,0 +1,31 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.AiUserHeartbeatRank; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.math.BigDecimal; +import java.util.List; + +public interface AiUserHeartbeatRankDao extends BaseMapper { + + /** + * 更新数据 + * @param id + * @param heartbeatVal + * @return + */ + @Update("update ai_user_heartbeat_rank set heartbeat_val_total = if(heartbeat_val_total + #{heartbeatVal} > 0, heartbeat_val_total + #{heartbeatVal}, 0) where id = #{id}") + Integer incrementHeartbeatVal(@Param("id") Long id, @Param("heartbeatVal") BigDecimal heartbeatVal); + + /** + * 获取榜单数据 + * @param ps + * @return + */ + @Select("select user_id from ai_user_heartbeat_rank order by heartbeat_val_total desc limit #{ps}") + List getAiUserHeartbeatRankList(@Param("ps") Integer ps); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserHeartbeatRelationDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserHeartbeatRelationDao.java new file mode 100644 index 0000000..0c84db2 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserHeartbeatRelationDao.java @@ -0,0 +1,53 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.sonic.frog.domain.entity.AiUserHeartbeatRelation; +import com.sonic.frog.domain.input.HeartbeatRelationListInput; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 用户与AI的心动关系数据库访问层 + */ +@Mapper +public interface AiUserHeartbeatRelationDao extends BaseMapper { + /** + * 获取最近90天当前用户与AI的心动值排名 + * + * @param userId + * @param aiId + * @return + */ + Integer getUserHeartbeatRank(@Param("userId") Long userId, @Param("aiId") Long aiId); + + /** + * 获取最近90天与该AI达到初识关系的人数 + * + * @param aiId + * @return + */ + Integer getUserHeartbeatCount(@Param("aiId") Long aiId); + + /** + * 心动关系列表 + * + * @param page + * @param input + * @return + */ + IPage heartbeatRelationListPage(Page page, @Param("input") HeartbeatRelationListInput input); + + /** + * 24小时未聊天的关系列表 + * + * @param calcTime + * @return + */ + List hours24NoChatRelationList(LocalDateTime calcTime); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserStatDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserStatDao.java new file mode 100644 index 0000000..1e62497 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/AiUserStatDao.java @@ -0,0 +1,116 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.bo.AiGiftRankBo; +import com.sonic.frog.domain.bo.AiHeartbeatRankBo; +import com.sonic.frog.domain.bo.ChatRankBo; +import com.sonic.frog.domain.entity.AiUserStat; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +import java.math.BigDecimal; +import java.util.List; + +/** + * AI用户统计数据访问接口 + */ +@Mapper +public interface AiUserStatDao extends BaseMapper { + /** + * 更新点赞数 + * + * @param aiId + * @param num + */ + void updateLikedNum(@Param("aiId") Long aiId, @Param("num") Integer num); + + /** + * 更新点赞数 + * + * @param aiId + * @param num + */ + void updateDislikedNum(@Param("aiId") Long aiId, @Param("num") Integer num); + + /** + * 添加聊天数 + * + * @param aiId + * @param num + */ + void increaseChatNum(@Param("aiId") Long aiId, @Param("num") Integer num); + + /** + * 更新会话数 + * + * @param aiId + */ + void increaseConversationNum(@Param("aiId") Long aiId); + + /** + * 添加金币数 + * + * @param aiId + * @param coinNum + */ + void increaseCoinNum(@Param("aiId") Long aiId, @Param("coinNum") Long coinNum); + + /** + * 添加礼物金币数 + * + * @param aiId + * @param coinNum + */ + void increaseGiftCoinNum(@Param("aiId") Long aiId, @Param("coinNum") Long coinNum); + + /** + * 添加解锁图片金币数 + * + * @param aiId + * @param coinNum + */ + void increaseUnlockImgCoinNum(@Param("aiId") Long aiId, @Param("coinNum") Long coinNum); + + /** + * 查询出喜欢数最多的AI + * + * @param exAiIds + * @param ps + * @return + */ + List queryLikedAiList(@Param("exAiIds") List exAiIds, @Param("exAlbumIdList") List exAlbumIdList, @Param("ps") int ps); + + /** + * 更新数据 + * + * @param aiId + * @param heartbeatVal + * @return + */ + @Update("update ai_user_stat set heartbeat_val_total = if(heartbeat_val_total + #{heartbeatVal} > 0, heartbeat_val_total + #{heartbeatVal}, 0) where ai_id = #{aiId}") + Integer incrementHeartbeatVal(@Param("aiId") Long aiId, @Param("heartbeatVal") BigDecimal heartbeatVal); + + + /** + * 查询热聊榜单 + * + * @return + */ + List queryChatRank(@Param("ps") Integer ps); + + /** + * 查询热聊榜单 + * + * @return + */ + List queryAiHeartbeatRank(@Param("ps") Integer ps); + + /** + * 查询礼物榜单 + * + * @param ps + * @return + */ + List queryAiGiftRank(@Param("ps") Integer ps); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/BuyCreateCountRecordDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/BuyCreateCountRecordDao.java new file mode 100644 index 0000000..1339e46 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/BuyCreateCountRecordDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.BuyCreateCountRecord; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户购买相册创作次数记录 Dao + */ +@Mapper +public interface BuyCreateCountRecordDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/BuyHeartbeatValueRecordDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/BuyHeartbeatValueRecordDao.java new file mode 100644 index 0000000..27ccade --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/BuyHeartbeatValueRecordDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.BuyHeartbeatValueRecord; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户购买心动值记录数据库访问层 + */ +@Mapper +public interface BuyHeartbeatValueRecordDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatBubbleDictDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatBubbleDictDao.java new file mode 100644 index 0000000..0fe5509 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatBubbleDictDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.ChatBubbleDict; +import org.apache.ibatis.annotations.Mapper; + +/** + * 聊天气泡字典数据库访问层 + */ +@Mapper +public interface ChatBubbleDictDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatModelDictDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatModelDictDao.java new file mode 100644 index 0000000..cf95a7c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatModelDictDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.ChatModelDict; +import org.apache.ibatis.annotations.Mapper; + +/** + * 对话模型字典数据库访问层 + */ +@Mapper +public interface ChatModelDictDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatSetDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatSetDao.java new file mode 100644 index 0000000..34f9a8e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatSetDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.ChatSet; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户与Ai的聊天设定表数据库访问层 + */ +@Mapper +public interface ChatSetDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatUserBackgroundDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatUserBackgroundDao.java new file mode 100644 index 0000000..dafc34e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/ChatUserBackgroundDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.ChatUserBackground; +import org.apache.ibatis.annotations.Mapper; + +/** + * 对话用户聊天背景数据库访问层 + */ +@Mapper +public interface ChatUserBackgroundDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/DemoDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/DemoDao.java new file mode 100644 index 0000000..a0031a1 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/DemoDao.java @@ -0,0 +1,38 @@ +package com.sonic.frog.dao; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.Demo; + +/** + * Demo的对象访问对象. BaseMapper会封装对数据库的CRUD访问. + * mybatis-plus文档: https://mybatis.plus/guide/ + */ +@Repository +public interface DemoDao extends BaseMapper { + + /** + * 使用mybatis映射文件来定义Dao方法,Mapper文件存放在resources/mapper里面 + * 详细的文档请参看: https://mybatis.org/mybatis-3/zh/sqlmap-xml.html + * + * 注意:这里仅仅是示例,一般情况还是推荐使用MybatisPlus来操作数据库 + * 目前来看仅当需要多表联查的时候,才可能会用到映射文件; + * 而多表联查本身并不推荐,可能会引起性能问题 + * @param status + * + * @return list of demo + */ + List listByStatus(@Param("status") Demo.Status status); + + /** + * update name + * + * @param id + * @param name + */ + void updateName(@Param("id") Long id, @Param("name") String name); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/GiftDictDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/GiftDictDao.java new file mode 100644 index 0000000..3cfd060 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/GiftDictDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.GiftDict; +import org.apache.ibatis.annotations.Mapper; + +/** + * 礼物字典数据访问接口 + */ +@Mapper +public interface GiftDictDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/GiftRewardRecordDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/GiftRewardRecordDao.java new file mode 100644 index 0000000..5561ae1 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/GiftRewardRecordDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.GiftRewardRecord; +import org.apache.ibatis.annotations.Mapper; + +/** + * 礼物打赏记录数据库访问层 + */ +@Mapper +public interface GiftRewardRecordDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/HeartbeatLevelDictDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/HeartbeatLevelDictDao.java new file mode 100644 index 0000000..5a17424 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/HeartbeatLevelDictDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.HeartbeatLevelDict; +import org.apache.ibatis.annotations.Mapper; + +/** + * 心动等级字典表数据库访问层 + */ +@Mapper +public interface HeartbeatLevelDictDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/ImageStyleDictDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/ImageStyleDictDao.java new file mode 100644 index 0000000..af65eea --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/ImageStyleDictDao.java @@ -0,0 +1,15 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.ImageStyleDict; + +/** + *

+ * 形象风格图片表 Mapper 接口 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +public interface ImageStyleDictDao extends BaseMapper { +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/LikedDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/LikedDao.java new file mode 100644 index 0000000..dfe25a4 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/LikedDao.java @@ -0,0 +1,17 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.Liked; +import org.apache.ibatis.annotations.Mapper; + +/** + * 点赞数据访问接口 + */ +@Mapper +public interface LikedDao extends BaseMapper { + /** + * 点赞或取消 + * @param liked 点赞或取消数据 + */ + void likeOrCancel(Liked liked); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/MeetUnlockDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/MeetUnlockDao.java new file mode 100644 index 0000000..9a91a17 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/MeetUnlockDao.java @@ -0,0 +1,7 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.MeetUnlock; + +public interface MeetUnlockDao extends BaseMapper { +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/SignInRecordDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/SignInRecordDao.java new file mode 100644 index 0000000..0991388 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/SignInRecordDao.java @@ -0,0 +1,8 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.SignInRecord; + +public interface SignInRecordDao extends BaseMapper { + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/SignInStatDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/SignInStatDao.java new file mode 100644 index 0000000..8cb7e8e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/SignInStatDao.java @@ -0,0 +1,8 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.SignInStat; + +public interface SignInStatDao extends BaseMapper { + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/TimbreDictDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/TimbreDictDao.java new file mode 100644 index 0000000..94a588c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/TimbreDictDao.java @@ -0,0 +1,12 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.TimbreDict; +import org.apache.ibatis.annotations.Mapper; + +/** + * 音色字典表数据库访问层 + */ +@Mapper +public interface TimbreDictDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/UserAiMeetDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/UserAiMeetDao.java new file mode 100644 index 0000000..57f24cd --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/UserAiMeetDao.java @@ -0,0 +1,16 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.UserAiMeet; + +/** + *

+ * 用户和AI相互喜欢记录表 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +public interface UserAiMeetDao extends BaseMapper { +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/UserCreateCountStatDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/UserCreateCountStatDao.java new file mode 100644 index 0000000..3c49446 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/UserCreateCountStatDao.java @@ -0,0 +1,30 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.UserCreateCountStat; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 用户相册创作次数统计 Dao + */ +@Mapper +public interface UserCreateCountStatDao extends BaseMapper { + + /** + * 增加购买次数 + * + * @param userId + * @param count + */ + void addBuyNum(@Param("userId") Long userId, @Param("count") Integer count); + + /** + * 获取等待赠送会员次数的列表 + * + * @return + */ + List getWaitingGiftMemberNumList(); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/dao/UserDeductionStatDao.java b/sonic-frog/server/src/main/java/com/sonic/frog/dao/UserDeductionStatDao.java new file mode 100644 index 0000000..60a618e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/dao/UserDeductionStatDao.java @@ -0,0 +1,19 @@ +package com.sonic.frog.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.frog.domain.entity.UserDeductionStat; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户聊天、语音,语音通话扣费统计表 Dao + */ +@Mapper +public interface UserDeductionStatDao extends BaseMapper { + /** + * 获取总的预扣金额 + * + * @param userId + * @return + */ + Long getTotalDeductionAmount(Long userId); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AIUserAlbumGroupBo.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AIUserAlbumGroupBo.java new file mode 100644 index 0000000..739ccff --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AIUserAlbumGroupBo.java @@ -0,0 +1,18 @@ +package com.sonic.frog.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AIUserAlbumGroupBo { + + Long aiId; + + private String idListStr; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AiGiftRankBo.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AiGiftRankBo.java new file mode 100644 index 0000000..4318650 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AiGiftRankBo.java @@ -0,0 +1,23 @@ +package com.sonic.frog.domain.bo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiGiftRankBo { + + private Long aiId; + + private Long giftCoinNum; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AiHeartbeatRankBo.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AiHeartbeatRankBo.java new file mode 100644 index 0000000..5d646ed --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AiHeartbeatRankBo.java @@ -0,0 +1,23 @@ +package com.sonic.frog.domain.bo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiHeartbeatRankBo { + + private Long aiId; + + private BigDecimal heartbeatValTotal; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AiUserCacheInfo.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AiUserCacheInfo.java new file mode 100644 index 0000000..b158383 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/AiUserCacheInfo.java @@ -0,0 +1,50 @@ +package com.sonic.frog.domain.bo; + +import com.sonic.frog.domain.entity.ImageStyleDict; +import com.sonic.frog.domain.entity.TimbreDict; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @description: ai用户缓存信息,用于构建聊天系统提示词,图片提示词 + * @author: mzc + * @date: 2025-07-18 10:36 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserCacheInfo { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("AI所属用户id") + private Long userId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("出生日期") + private String profile; + + @ApiModelProperty("对话风格") + private String dialogueStyle; + + @ApiModelProperty("对话开场白") + private String dialoguePrologue; + + @ApiModelProperty("基图-首次创建AI时选择的形象图") + private String baseImageUrl; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/ChatRankBo.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/ChatRankBo.java new file mode 100644 index 0000000..463b580 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/ChatRankBo.java @@ -0,0 +1,22 @@ +package com.sonic.frog.domain.bo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChatRankBo { + + private Long aiId; + + private Integer chatNum; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/RandomMeetRateBo.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/RandomMeetRateBo.java new file mode 100644 index 0000000..e406a1d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/RandomMeetRateBo.java @@ -0,0 +1,29 @@ +package com.sonic.frog.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RandomMeetRateBo implements Serializable { + + /** + * 11-20张卡片中是否中 + */ + private Boolean bl1120; + + private Boolean bl3140; + + private Boolean bl4150; + + private Boolean bl5160; + + private Boolean bl6170; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/ThirdAuthBo.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/ThirdAuthBo.java new file mode 100644 index 0000000..22d9d50 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/bo/ThirdAuthBo.java @@ -0,0 +1,25 @@ +package com.sonic.frog.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author code + * @Description 三方认证 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ThirdAuthBo { + + private String thirdId; + + private String nickname; + + private String email; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/Advertise.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/Advertise.java new file mode 100644 index 0000000..79cee44 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/Advertise.java @@ -0,0 +1,110 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.sonic.frog.enums.AdvertiseBizType; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 广告管理 + */ +@Data +@TableName("advertise") +public class Advertise { + + /** + * 主键id + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 业务类型 + */ + @TableField("biz_type") + private AdvertiseBizType bizType; + + /** + * 广告名称 + */ + @TableField("name") + private String name; + + /** + * 广告配图 + */ + @TableField("icon") + private String icon; + + /** + * 跳转连接 + */ + @TableField("jump_link") + private String jumpLink; + + /** + * 展示开始时间 + */ + @TableField("show_start_time") + private LocalDateTime showStartTime; + + /** + * 展示结束时间 + */ + @TableField("show_end_time") + private LocalDateTime showEndTime; + + /** + * 扩展字段 + */ + @TableField("ext") + private String ext; + + /** + * 排序 + */ + @TableField("sort") + private Integer sort; + + /** + * 使用端点(WEB/ANDROID/IOS) + */ + @TableField("endpoint") + private String endpoint; + + /** + * 是否删除(1.是,0.否) + */ + @TableField("is_delete") + private Boolean isDelete; + + /** + * 是否弹窗(1.是,0.否) + */ + @TableField("is_global") + private Integer isGlobal; + + /** + * 创建人 + */ + @TableField("creator_id") + private Long creatorId; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 更新人 + */ + @TableField("editor_id") + private Long editorId; + + /** + * 更新时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiDict.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiDict.java new file mode 100644 index 0000000..5721c5d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiDict.java @@ -0,0 +1,58 @@ +package com.sonic.frog.domain.entity; +import com.baomidou.mybatisplus.annotation.*; +import com.sonic.frog.enums.AiDictTypeEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import java.time.LocalDateTime; + +/** + *

+ * AI角色,性格,标签,形象风格字典表 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("ai_dict") +@ApiModel(value = "AiDict对象", description = "AI角色,性格,标签,形象风格字典表") +public class AiDict { + + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty(value = "主键ID") + private Integer id; + + @TableField("code") + @ApiModelProperty(value = "Code唯一") + private String code; + + @TableField("name") + @ApiModelProperty(value = "名称") + private String name; + + @TableField("parent_code") + @ApiModelProperty(value = "父级Code") + private String parentCode; + + @TableField("type") + @ApiModelProperty(value = "类型 ROLE:角色 CHARACTER:性格 TAG:标签") + private AiDictTypeEnum type; + + @TableField("sort") + @ApiModelProperty(value = "排序") + private Integer sort; + + @TableField("is_delete") + @ApiModelProperty(value = "是否删除(1.是,0.否)") + private Boolean isDelete; + + @TableField("create_time") + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUser.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUser.java new file mode 100644 index 0000000..7283c4d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUser.java @@ -0,0 +1,114 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Period; +import java.time.ZoneId; +import java.util.Date; + +/** + * AI用户表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("ai_user") +@ApiModel("AI用户表") +public class AiUser { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty("AI的id") + @TableField("ai_id") + private Long aiId; + + @ApiModelProperty("AI所属主人用户id") + @TableField("user_id") + private Long userId; + + @ApiModelProperty("AI用户对外展示ID") + @TableField("id_card") + private String idCard; + + @ApiModelProperty("昵称") + @TableField("nickname") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + @TableField("sex") + private Integer sex; + + @ApiModelProperty("头像") + @TableField("head_img") + private String headImg; + + @ApiModelProperty("出生日期") + @TableField("birthday") + private LocalDateTime birthday; + + @ApiModelProperty("角色code 对应ai_dict表中code") + @TableField("role_code") + private String roleCode; + + @ApiModelProperty("性格code 对应ai_dict表中code") + @TableField("character_code") + private String characterCode; + + @ApiModelProperty("标签code 对应ai_dict表中code") + @TableField("tag_code") + private String tagCode; + + @ApiModelProperty("简介") + @TableField("introduction") + private String introduction; + + @ApiModelProperty("权限 1: 公开 2:私密") + @TableField("permission") + private Integer permission; + + @ApiModelProperty("形象图") + @TableField("image_url") + private String imageUrl; + + @ApiModelProperty("主页头图-创建编辑时会改变") + @TableField("home_image_url") + private String homeImageUrl; + + @ApiModelProperty("基图-首次创建AI时选择的形象图") + @TableField("base_image_url") + private String baseImageUrl; + + @ApiModelProperty("最后一次聊天时间") + @TableField("last_chat_time") + private LocalDateTime lastChatTime; + + @ApiModelProperty("是否删除(1.是,0.否)") + @TableField("is_delete") + private Boolean isDelete; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("修改人") + @TableField("editor_id") + private Long editorId; + + @ApiModelProperty("更新时间") + @TableField("edit_time") + private LocalDateTime editTime; + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserAlbum.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserAlbum.java new file mode 100644 index 0000000..89c9256 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserAlbum.java @@ -0,0 +1,103 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * AI用户相册表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("ai_user_album") +public class AiUserAlbum { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * AI的id + */ + @TableField("ai_id") + private Long aiId; + + @ApiModelProperty("AI所属主人用户id") + @TableField("user_id") + private Long userId; + + /** + * 图片地址 + */ + @TableField("img_url") + private String imgUrl; + + /** + * 源图片地址 + */ + @TableField("source_img_url") + private String sourceImgUrl; + + /** + * 解锁价格 + */ + @TableField("unlock_price") + private Long unlockPrice; + + /** + * 图片宽 + */ + @TableField("width") + private String width; + + /** + * 图片高 + */ + @TableField("height") + private String height; + + /** + * 排序 + */ + @TableField("img_order") + private Integer imgOrder; + + /** + * 是否默认图片 + */ + @TableField("is_default") + private Boolean isDefault; + + /** + * 点赞数 + */ + @TableField("liked_count") + private Integer likedCount; + + /** + * 是否删除 (0:未删除, 1:删除) + */ + @TableField("is_delete") + private Boolean isDelete; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserAlbumUnlock.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserAlbumUnlock.java new file mode 100644 index 0000000..4575a70 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserAlbumUnlock.java @@ -0,0 +1,59 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * AI用户相册表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("ai_user_album_unlock") +public class AiUserAlbumUnlock { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 相册id + */ + @TableField("album_id") + private Long albumId; + + @ApiModelProperty("解锁的用户id") + @TableField("user_id") + private Long userId; + + /** + * 订单号 + */ + @TableField("order_no") + private String orderNo; + + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserExt.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserExt.java new file mode 100644 index 0000000..c500d3c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserExt.java @@ -0,0 +1,109 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * AI用户扩展表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("ai_user_ext") +@ApiModel("AI用户扩展表") +public class AiUserExt { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty("AI的id") + @TableField("ai_id") + private Long aiId; + + @ApiModelProperty("AI所属主人用户id") + @TableField("user_id") + private Long userId; + + @ApiModelProperty("人物设定") + @TableField("profile") + private String profile; + + @ApiModelProperty("人物设定 用户输入") + @TableField("user_profile") + private String userProfile; + + @ApiModelProperty("用户基础信息 扩展内容") + @TableField("user_profile_ext_json") + private String userProfileExtJson; + + @ApiModelProperty("对话风格 用户输入") + @TableField("user_dialogue_style") + private String userDialogueStyle; + + @ApiModelProperty("对话风格") + @TableField("dialogue_style") + private String dialogueStyle; + + @ApiModelProperty("对话开场白") + @TableField("dialogue_prologue") + private String dialoguePrologue; + + @ApiModelProperty("对话开场白 语音") + @TableField("dialogue_prologue_sound") + private String dialoguePrologueSound; + + @ApiModelProperty("对话音色code") + @TableField("dialogue_timbre_code") + private String dialogueTimbreCode; + + @ApiModelProperty("对话-音高") + @TableField("dialogue_pitch") + private String dialoguePitch; + + @ApiModelProperty("对话-语速") + @TableField("dialogue_speech_rate") + private String dialogueSpeechRate; + + @ApiModelProperty("形象风格code 对应image_style_dict表中code") + @TableField("image_style_code") + private String imageStyleCode; + + @ApiModelProperty("形象描述") + @TableField("image_desc") + private String imageDesc; + + @ApiModelProperty("形象参考") + @TableField("image_reference_url") + private String imageReferenceUrl; + + @ApiModelProperty("辅助聊天内容") + @TableField("supporting_content") + private String supportingContent; + + @ApiModelProperty("是否删除(1.是,0.否)") + @TableField("is_delete") + private Boolean isDelete; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("修改人") + @TableField("editor_id") + private Long editorId; + + @ApiModelProperty("更新时间") + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserGift.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserGift.java new file mode 100644 index 0000000..73ffc56 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserGift.java @@ -0,0 +1,56 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * AI用户收到礼物表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("ai_user_gift") +public class AiUserGift { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * AI的id + */ + @TableField("ai_id") + private Long aiId; + + /** + * 礼物id + */ + @TableField("gift_id") + private Integer giftId; + + /** + * 收到礼物数量 + */ + @TableField("num") + private Integer num; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserHeartbeatRank.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserHeartbeatRank.java new file mode 100644 index 0000000..0b78a23 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserHeartbeatRank.java @@ -0,0 +1,43 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 当前用户和所有聊天过的AI的心动值总和榜单 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("ai_user_heartbeat_rank") +public class AiUserHeartbeatRank { + + @ApiModelProperty("主键ID") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty("用户id") + @TableField("user_id") + private Long userId; + + @ApiModelProperty("当前用户心动值总和") + @TableField("heartbeat_val_total") + private BigDecimal heartbeatValTotal; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("编辑时间") + @TableField("edit_time") + private LocalDateTime editTime; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserHeartbeatRelation.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserHeartbeatRelation.java new file mode 100644 index 0000000..931205a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserHeartbeatRelation.java @@ -0,0 +1,95 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * 用户与AI的心动关系 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("ai_user_heartbeat_relation") +@ApiModel("用户与AI的心动关系") +public class AiUserHeartbeatRelation { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty("用户id") + @TableField("user_id") + private Long userId; + + @ApiModelProperty("AI的id") + @TableField("ai_id") + private Long aiId; + + @ApiModelProperty("对应心动等级字典code") + @TableField("heartbeat_level") + private HeartbeatLevelEnum heartbeatLevel; + + @ApiModelProperty("当前用户心动值") + @TableField("heartbeat_val") + private BigDecimal heartbeatVal; + + @ApiModelProperty("心动分") + @TableField("heartbeat_score") + private BigDecimal heartbeatScore; + + @ApiModelProperty("已扣减心动值") + @TableField("subtract_heartbeat_val") + private BigDecimal subtractHeartbeatVal; + + @ApiModelProperty("首次聊天时间 用于计算相识天数") + @TableField("first_chat_time") + private LocalDateTime firstChatTime; + + @ApiModelProperty("最后聊天时间 用于24小时未聊天计算扣减心动值") + @TableField("last_chat_time") + private LocalDateTime lastChatTime; + + @ApiModelProperty("最后一次计算扣减心动值时间") + @TableField("last_subtract_time") + private LocalDateTime lastSubtractTime; + + @ApiModelProperty("关系显示开关 默认关闭 0:关闭 1:打开") + @TableField("is_show") + private Boolean isShow; + + @ApiModelProperty("是否第一次降级 0:否 1:是") + @TableField("is_first_downgrade") + private Boolean isFirstDowngrade; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("编辑时间") + @TableField("edit_time") + private LocalDateTime editTime; + + /** + * 计算相识天数 + * + * @return + */ + public Integer getDayCount() { + if (firstChatTime == null) { + return 0; + } + long startTime = firstChatTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long endTime = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + return (int) ((endTime - startTime) / (1000 * 60 * 60 * 24)); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserStat.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserStat.java new file mode 100644 index 0000000..97fe0d0 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/AiUserStat.java @@ -0,0 +1,93 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * AI用户统计表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("ai_user_stat") +public class AiUserStat { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * AI的id + */ + @TableField("ai_id") + private Long aiId; + + /** + * 被喜欢数 + */ + @TableField("liked_num") + private Integer likedNum; + + /** + * 被不喜欢数 + */ + @TableField("disliked_num") + private Integer dislikedNum; + + /** + * 聊天数 + */ + @TableField("chat_num") + private Integer chatNum; + + /** + * 产生过对话的人数 + */ + @TableField("conversation_num") + private Integer conversationNum; + + /** + * 礼物赚取的Crush Coin数 + */ + @TableField("gift_coin_num") + private Long giftCoinNum; + + /** + * 解锁图片赚取的Crush Coin数 + */ + @TableField("unlock_img_coin_num") + private Long unlockImgCoinNum; + + /** + * 赚取的Crush Coin数 + */ + @TableField("coin_num") + private Long coinNum; + + /** + * 心动值总和 + */ + @TableField("heartbeat_val_total") + private BigDecimal heartbeatValTotal; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/BuyCreateCountRecord.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/BuyCreateCountRecord.java new file mode 100644 index 0000000..60329dc --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/BuyCreateCountRecord.java @@ -0,0 +1,55 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 用户购买相册创作次数记录 + */ +@Data +@TableName("buy_create_count_record") +public class BuyCreateCountRecord { + + /** + * 主键id + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 用户id + */ + @TableField("user_id") + private Long userId; + + /** + * 购买创作次数 + */ + @TableField("buy_num") + private Integer buyNum; + + /** + * 总金额 + */ + @TableField("total_amount") + private Long totalAmount; + + /** + * 订单号 + */ + @TableField("order_no") + private String orderNo; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/BuyHeartbeatValueRecord.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/BuyHeartbeatValueRecord.java new file mode 100644 index 0000000..8cfaf85 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/BuyHeartbeatValueRecord.java @@ -0,0 +1,62 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户购买心动值记录 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("buy_heartbeat_value_record") +@ApiModel("用户购买心动值记录") +public class BuyHeartbeatValueRecord { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty("用户id") + @TableField("user_id") + private Long userId; + + @ApiModelProperty("AI的id") + @TableField("ai_id") + private Long aiId; + + @ApiModelProperty("当前用户心动值") + @TableField("heartbeat_val") + private java.math.BigDecimal heartbeatVal; + + @ApiModelProperty("心动值单价") + @TableField("price") + private java.math.BigDecimal price; + + @ApiModelProperty("总金额") + @TableField("total_amount") + private java.math.BigDecimal totalAmount; + + @ApiModelProperty("订单编号") + @TableField("order_no") + private String orderNo; + + @ApiModelProperty("交易号") + @TableField("trade_no") + private String tradeNo; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("编辑时间") + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatBubbleDict.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatBubbleDict.java new file mode 100644 index 0000000..891e3ad --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatBubbleDict.java @@ -0,0 +1,77 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.sonic.frog.enums.BubbleUnlockTypeEnum; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 聊天气泡字典 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("chat_bubble_dict") +@ApiModel("聊天气泡字典") +public class ChatBubbleDict { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + @ApiModelProperty("code") + @TableField("code") + private String code; + + @ApiModelProperty("名称") + @TableField("name") + private String name; + + @ApiModelProperty("颜色") + @TableField("color") + private String color; + + @ApiModelProperty("图片url") + @TableField("img_url") + private String imgUrl; + + @ApiModelProperty("web端图片url") + @TableField("web_img_url") + private String webImgUrl; + + + @ApiModelProperty("解锁类型 MEMBER:会员 HEARTBEAT_LEVEL:心动等级") + @TableField("unlock_type") + private BubbleUnlockTypeEnum unlockType; + + @ApiModelProperty("解锁心动等级 类型为HEARTBEAT_LEVEL时才有用") + @TableField("unlock_heartbeat_level") + private HeartbeatLevelEnum unlockHeartbeatLevel; + + @ApiModelProperty("排序") + @TableField("sort") + private Integer sort; + + @ApiModelProperty("是否默认聊天气泡 1:是 0:不是") + @TableField("is_default") + private Boolean isDefault; + + @ApiModelProperty("是否删除 0:未删除 1:删除") + @TableField("is_delete") + private Boolean isDelete; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("更新时间") + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatModelDict.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatModelDict.java new file mode 100644 index 0000000..17af11f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatModelDict.java @@ -0,0 +1,74 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 对话模型字典 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("chat_model_dict") +@ApiModel("对话模型字典") +public class ChatModelDict { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + @ApiModelProperty("对话模型code") + @TableField("code") + private String code; + + @ApiModelProperty("对话模型名称") + @TableField("name") + private String name; + + @ApiModelProperty("对话模型描述") + @TableField("description") + private String description; + + @ApiModelProperty("文本价格") + @TableField("text_price") + private Long textPrice; + + @ApiModelProperty("语音价格") + @TableField("voice_price") + private Long voicePrice; + + @ApiModelProperty("语音聊天价格") + @TableField("voice_chat_price") + private Long voiceChatPrice; + + @ApiModelProperty("问号图标内容") + @TableField("question_mark") + private String questionMark; + + @ApiModelProperty("排序") + @TableField("sort") + private Integer sort; + + @ApiModelProperty("是否默认对话模型 1:是 0:不是") + @TableField("is_default") + private Boolean isDefault; + + @ApiModelProperty("是否删除 0:未删除 1:删除") + @TableField("is_delete") + private Boolean isDelete; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("更新时间") + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatSet.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatSet.java new file mode 100644 index 0000000..5050fd7 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatSet.java @@ -0,0 +1,83 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户与Ai的聊天设定表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("chat_set") +@ApiModel("用户与Ai的聊天设定表") +public class ChatSet { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty("用户id") + @TableField("user_id") + private Long userId; + + @ApiModelProperty("AI的id") + @TableField("ai_id") + private Long aiId; + + @ApiModelProperty("昵称") + @TableField("nickname") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + @TableField("sex") + private Integer sex; + + @ApiModelProperty("出生日期") + @TableField("birthday") + private LocalDateTime birthday; + + @ApiModelProperty("我是谁") + @TableField("who_am_i") + private String whoAmI; + + @ApiModelProperty("对话模型code") + @TableField("model_code") + private String modelCode; + + @ApiModelProperty("聊天气泡code") + @TableField("bubble_code") + private String bubbleCode; + + @ApiModelProperty("聊天背景图片") + @TableField("background_img") + private String backgroundImg; + + @ApiModelProperty("自动播放语音开关 1:开 0:关") + @TableField("is_auto_play_voice") + private Integer isAutoPlayVoice; + + + @ApiModelProperty("是否删除聊天消息 1:是 0:否") + @TableField("is_del_chatted") + private Boolean isDelChatted; + + @ApiModelProperty("是否删除 0:未删除 1:已删除") + @TableField("is_delete") + private Integer isDelete; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("编辑时间") + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatUserBackground.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatUserBackground.java new file mode 100644 index 0000000..5bae9c6 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ChatUserBackground.java @@ -0,0 +1,62 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 对话用户聊天背景 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("chat_user_background") +@ApiModel("对话用户聊天背景") +public class ChatUserBackground { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty("对话用户id") + @TableField("user_id") + private Long userId; + + @ApiModelProperty("AI的id") + @TableField("ai_id") + private Long aiId; + + @ApiModelProperty("图片地址") + @TableField("img_url") + private String imgUrl; + + @ApiModelProperty("图片宽") + @TableField("width") + private String width; + + @ApiModelProperty("图片高") + @TableField("height") + private String height; + + @ApiModelProperty("排序") + @TableField("img_order") + private Integer imgOrder; + + @ApiModelProperty("是否删除") + @TableField("is_delete") + private Boolean isDelete; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("更新时间") + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/Demo.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/Demo.java new file mode 100644 index 0000000..3eb2395 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/Demo.java @@ -0,0 +1,53 @@ +package com.sonic.frog.domain.entity; + +import java.time.LocalDateTime; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * XXX: 实体声明的时候,必须保留@NoArgsConstructor注解,否则mybatisPlus在反序列化实体的时候会报错 + * mybatis-plus文档: https://mybatis.plus/guide/ + * @author code + */ +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "demo", autoResultMap = true) +public class Demo { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField + private String name; + + @TableField + private Status status; + + @TableField(typeHandler = FastjsonTypeHandler.class) + private JSONObject ext; + + @TableField + private LocalDateTime createdAt; + + @TableField + private LocalDateTime updatedAt; + + public enum Status { + /** 状态 */ + ENABLED, + DISABLED, + ; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GenImageTask.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GenImageTask.java new file mode 100644 index 0000000..696ee2d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GenImageTask.java @@ -0,0 +1,100 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 生成图片任务主表 + * @author zzhan + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "gen_image_task", autoResultMap = true) +public class GenImageTask { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 批次号 + */ + private String batchNo; + + /** + * 用户ID + */ + private Long userId; + + /** + * 状态(PENDING 处理中、RELEASED 已释放) + */ + private Status status; + + /** + * 任务总数 + */ + private Integer taskCount; + + /** + * 已经有多少张图片任务获取到结果了 + */ + private Integer completedCount; + + /** + * 前端轮询次数 + */ + private Integer pollingCount; + + /** + * 最后一次心跳时间 + */ + private LocalDateTime heartBeatTime; + + /** + * 终端类型 + */ + private String endpoint; + + /** + * 设备号 + */ + private String deviceId; + + /** + * IP地址 + */ + private String ipAddress; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + private LocalDateTime editTime; + + + public enum Status { + /** + * 处理中、已释放 + */ + PENDING, + RELEASED; + + } + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GenImageTaskRecord.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GenImageTaskRecord.java new file mode 100644 index 0000000..ea56a20 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GenImageTaskRecord.java @@ -0,0 +1,105 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 生成图片任务子项表 + * @author zzhan + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "gen_image_task_record", autoResultMap = true) +public class GenImageTaskRecord { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 任务ID gen_image_task表主键id + */ + private Long taskId; + + /** + * 批次号 + */ + private String batchNo; + + /** + * 用户ID + */ + private Long userId; + + /** + * 生成图片的第三方任务id + */ + private String thirdTaskId; + + /** + * 生成图片的SD提示词 + */ + private String prompt; + + /** + * 图片链接 + */ + private String imageUrl; + + /** + * 图片连接的MD5值 + */ + private String imageUrlMd5; + + /** + * 图片生成状态 + */ + private Status status; + + /** + * 是否已生成完成(0 否、1 是) + */ + private Boolean completed; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + private LocalDateTime editTime; + + /** + * 图片生成结果状态 + */ + public enum Status { + /** + * 生成中 + * 被鉴黄(终态) + * 生成完成(终态) + * 失败 模型接口报错时 + */ + PENDING, + NSFW, + COMPLETED, + FAILED, + + ; + } + + + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GiftDict.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GiftDict.java new file mode 100644 index 0000000..2867392 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GiftDict.java @@ -0,0 +1,88 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 礼物字典表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("gift_dict") +public class GiftDict { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 礼物名称 + */ + @TableField("name") + private String name; + + /** + * 礼物单价 对应Coin数量 + */ + @TableField("price") + private Long price; + + /** + * 礼物icon + */ + @TableField("icon") + private String icon; + + /** + * 排序 + */ + @TableField("sort") + private Integer sort; + + /** + * 礼物说明 + */ + @TableField("`desc`") + private String desc; + + + /** + * 发送该礼物需要的心动等级 + */ + @TableField("heartbeat_level") + private HeartbeatLevelEnum heartbeatLevel; + + + @ApiModelProperty("是否会员礼物") + @TableField("is_member_gift") + private Boolean isMemberGift; + + /** + * 是否删除 (0:未删除, 1:删除) + */ + @TableField("is_delete") + private Boolean isDelete; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 创建人 + */ + @TableField("creator_id") + private Long creatorId; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GiftRewardRecord.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GiftRewardRecord.java new file mode 100644 index 0000000..14518a8 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/GiftRewardRecord.java @@ -0,0 +1,78 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 礼物打赏记录 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("gift_reward_record") +@ApiModel("礼物打赏记录") +public class GiftRewardRecord { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty("打赏礼物的用户id") + @TableField("from_uid") + private Long fromUid; + + @ApiModelProperty("被打赏礼物AI用户id") + @TableField("to_uid") + private Long toUid; + + @ApiModelProperty("订单编号") + @TableField("order_no") + private String orderNo; + + @ApiModelProperty("礼物id") + @TableField("gift_id") + private Integer giftId; + + @ApiModelProperty("礼物名称") + @TableField("gift_name") + private String giftName; + + @ApiModelProperty("礼物单价 E-coin数量 ") + @TableField("price") + private Long price; + + @ApiModelProperty("本次打赏礼物个数") + @TableField("num") + private Integer num; + + @ApiModelProperty("总打赏额") + @TableField("total") + private Long total; + + @ApiModelProperty("用户实际的收入(扣除平台抽成后的收入)") + @TableField("income_total") + private Long incomeTotal; + + @ApiModelProperty("平台手续费(平台抽成)") + @TableField("platform_fee") + private Long platformFee; + + @ApiModelProperty("状态(ENABLED、DISABLED)") + @TableField("status") + private String status; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("创建人id") + @TableField("creator_id") + private Integer creatorId; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/HeartbeatLevelDict.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/HeartbeatLevelDict.java new file mode 100644 index 0000000..c40c5cc --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/HeartbeatLevelDict.java @@ -0,0 +1,71 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 心动等级字典表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("heartbeat_level_dict") +@ApiModel("心动等级字典表") +public class HeartbeatLevelDict { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + @ApiModelProperty("心动等级code") + @TableField("code") + private HeartbeatLevelEnum code; + + @ApiModelProperty("心动等级名称") + @TableField("name") + private String name; + + @ApiModelProperty("心动等级图标") + @TableField("img_url") + private String imgUrl; + + @ApiModelProperty("心动等级未解锁图标") + @TableField("unlock_img_url") + private String unlockImgUrl; + + @ApiModelProperty("解锁当前等级的心动值开始值") + @TableField("start_val") + private java.math.BigDecimal startVal; + + @ApiModelProperty("解锁当前等级的心动值结束值") + @TableField("end_val") + private java.math.BigDecimal endVal; + + @ApiModelProperty("解锁当前等级后包含的心动等级code") + @TableField("unlock_code") + private String unlockCode; + + @ApiModelProperty("排序") + @TableField("sort") + private Integer sort; + + @ApiModelProperty("是否删除 0:未删除 1:删除") + @TableField("is_delete") + private Boolean isDelete; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; + + @ApiModelProperty("编辑时间") + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ImageStyleDict.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ImageStyleDict.java new file mode 100644 index 0000000..b99afa4 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/ImageStyleDict.java @@ -0,0 +1,63 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + *

+ * 形象风格图片表 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("image_style_dict") +@ApiModel(value = "ImageStylePic对象", description = "形象风格图片表") +public class ImageStyleDict { + + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty(value = "主键ID") + private Integer id; + + @TableField("code") + @ApiModelProperty(value = "风格code") + private String code; + + @TableField("name") + @ApiModelProperty(value = "风格名称") + private String name; + + @TableField("url") + @ApiModelProperty(value = "风格对应的图片url") + private String url; + + @TableField("prompt") + @ApiModelProperty(value = "风格对应的图片prompt") + private String prompt; + + @TableField("sort") + @ApiModelProperty(value = "排序") + private Integer sort; + + @TableField("is_delete") + @ApiModelProperty(value = "是否删除(1.是,0.否)") + private Integer isDelete; + + @TableField("create_time") + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/Liked.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/Liked.java new file mode 100644 index 0000000..86f7885 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/Liked.java @@ -0,0 +1,83 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 点赞表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("liked") +public class Liked { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 点赞内容的 Id + */ + @TableField("biz_id") + private Long bizId; + + /** + * 点赞内容的类型 (ALBUM_PIC 等) + */ + @TableField("biz_type") + private BizType bizType; + + /** + * 被点赞用户ai Id + */ + @TableField("ai_id") + private Long aiId; + + /** + * 点赞用户 id + */ + @TableField("liked_user_id") + private Long likedUserId; + + /** + * 点赞状态(LIKE已点赞、CANCELED取消点赞) + */ + @TableField("liked_status") + private LikedStatus likedStatus; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; + + + public enum BizType { + /** 点赞的内容 */ + ALBUM_PIC, + AI, + ; + } + + public enum LikedStatus { + /** 状态 已点赞、取消点赞*/ + LIKED, + CANCELED, + ; + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/MeetUnlock.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/MeetUnlock.java new file mode 100644 index 0000000..a08bf8f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/MeetUnlock.java @@ -0,0 +1,50 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * Meet解锁记录实体类 + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +@TableName("meet_unlock") +public class MeetUnlock { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + @TableField("user_id") + private Long userId; + + /** + * AI的ID + */ + @TableField("ai_id") + private Long aiId; + + /** + * 订单号 + */ + @TableField("order_no") + private String orderNo; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/SignInRecord.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/SignInRecord.java new file mode 100644 index 0000000..aebe2ac --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/SignInRecord.java @@ -0,0 +1,40 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "sign_in_record", autoResultMap = true) +public class SignInRecord { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * PST 天(yyyy-MM-dd) + */ + private String dayStr; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/SignInStat.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/SignInStat.java new file mode 100644 index 0000000..7d60f74 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/SignInStat.java @@ -0,0 +1,60 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "sign_in_stat", autoResultMap = true) +public class SignInStat { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 本轮PST 开始天(yyyy-MM-dd) + */ + private String startDay; + + /** + * 本轮PST 结束天(yyyy-MM-dd) + */ + private String endDay; + + /** + * 当前连续签到天数 + */ + private Integer allDays; + + /** + * 是否删除(0 否、1 是) + */ + private Boolean isDelete; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/TimbreDict.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/TimbreDict.java new file mode 100644 index 0000000..ea8eba3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/TimbreDict.java @@ -0,0 +1,81 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 音色字典表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("timbre_dict") +@ApiModel("音色字典表") +public class TimbreDict { + @ApiModelProperty("id") + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + @ApiModelProperty("1:男性,2:女性") + @TableField("type") + private Integer type; + + @ApiModelProperty("音色code") + @TableField("code") + private String code; + + @ApiModelProperty("音色名称") + @TableField("name") + private String name; + + @ApiModelProperty("描述") + @TableField("description") + private String description; + + @ApiModelProperty("音频地址") + @TableField("url") + private String url; + + @ApiModelProperty("语音类型 第三方") + @TableField("voice_type") + private String voiceType; + + @ApiModelProperty("语音文本") + @TableField("voice_text") + private String voiceText; + + @ApiModelProperty("音高") + @TableField("pitch_rate") + private Integer pitchRate; + + @ApiModelProperty("语速") + @TableField("speech_rate") + private Integer speechRate; + + @ApiModelProperty("支持的语种") + @TableField("language") + private String language; + + @ApiModelProperty("支持的情感") + @TableField("support_emotions") + private String supportEmotions; + + @ApiModelProperty("是否删除(1.是,0.否)") + @TableField("is_delete") + private Integer isDelete; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/UserAiMeet.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/UserAiMeet.java new file mode 100644 index 0000000..edfd854 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/UserAiMeet.java @@ -0,0 +1,44 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.time.LocalDateTime; + +/** + *

+ * 用户和AI相互喜欢记录表 + *

+ * + * @author your-name + * @since 2024-11-28 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("user_ai_meet") +@ApiModel(value = "UserAiMeet对象", description = "用户和AI相互喜欢记录表") +public class UserAiMeet { + + @TableId(value = "id", type = IdType.NONE) + @ApiModelProperty(value = "主键ID") + private Long id; + + @TableField("user_id") + @ApiModelProperty(value = "用户ID") + private Long userId; + + @TableField("ai_id") + @ApiModelProperty(value = "AI的ID") + private Long aiId; + + @TableField("create_time") + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/UserCreateCountStat.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/UserCreateCountStat.java new file mode 100644 index 0000000..e89e398 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/UserCreateCountStat.java @@ -0,0 +1,91 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 用户相册创作次数统计 + */ +@Data +@TableName("user_create_count_stat") +public class UserCreateCountStat { + + /** + * 主键id + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 用户id + */ + @TableField("user_id") + private Long userId; + + /** + * 免费创作次数 + */ + @TableField("free_num") + private Integer freeNum; + + /** + * 免费创作次数 + */ + @TableField("used_free_num") + private Integer usedFreeNum; + + /** + * 会员赠送创作次数 + */ + @TableField("member_num") + private Integer memberNum; + /** + * 会员赠送创作次数 + */ + @TableField("used_member_num") + private Integer usedMemberNum; + + + /** + * 会员赠送创作次数年月 + */ + @TableField("member_year_month") + private String memberYearMonth; + + /** + * 下次赠送会员次数的时间 + */ + @TableField("next_gift_time") + private LocalDateTime nextGiftTime; + + /** + * 会员过期时间 + */ + @TableField("member_exp_time") + private LocalDateTime memberExpTime; + + /** + * 购买创作次数 + */ + @TableField("buy_num") + private Integer buyNum; + + /** + * 已使用购买创作次数 + */ + @TableField("used_buy_num") + private Integer usedBuyNum; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/UserDeductionStat.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/UserDeductionStat.java new file mode 100644 index 0000000..2485627 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/entity/UserDeductionStat.java @@ -0,0 +1,61 @@ +package com.sonic.frog.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 用户聊天、语音,语音通话扣费统计表 + */ +@Data +@TableName("user_deduction_stat") +public class UserDeductionStat { + + /** + * 主键id + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 用户id + */ + @TableField("user_id") + private Long userId; + + /** + * ai用户id + */ + @TableField("ai_id") + private Long aiId; + + /** + * 扣费类型 文本:1 发送或听取语音 :2 语音通话:3 + */ + @TableField("deduction_type") + private Integer deductionType; + + /** + * 预扣金额 + */ + @TableField("pre_deduction_amount") + private Long preDeductionAmount; + + /** + * 业务最后一次时间 + */ + @TableField("last_time") + private LocalDateTime lastTime; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AddBackgroundInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AddBackgroundInput.java new file mode 100644 index 0000000..dc433a3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AddBackgroundInput.java @@ -0,0 +1,26 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +/** + * @Author zzhan + * @Date 2020/7/10 14:24 + * @Version 1.0 + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AddBackgroundInput { + + @ApiModelProperty("图片地址") + private String url; + + @ApiModelProperty("图片宽") + private String width; + + @ApiModelProperty("图片宽") + private String height; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AgeRange.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AgeRange.java new file mode 100644 index 0000000..7a06f5d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AgeRange.java @@ -0,0 +1,24 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * @description: 年龄范围 + * @author: mzc + * @date: 2025-10-10 18:00 + **/ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AgeRange { + @ApiModelProperty("开始生日(内部使用)") + private LocalDateTime startBirthday; + + @ApiModelProperty("结束生日(内部使用)") + private LocalDateTime endBirthday; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiAlbumImageInfo.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiAlbumImageInfo.java new file mode 100644 index 0000000..8e409e0 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiAlbumImageInfo.java @@ -0,0 +1,29 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +/** + * @Author zzhan + * @Date 2020/7/10 14:24 + * @Version 1.0 + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AiAlbumImageInfo { + + @ApiModelProperty("图片地址") + private String url; + + @ApiModelProperty("图片宽") + private String width; + + @ApiModelProperty("图片宽") + private String height; + + @ApiModelProperty("解锁价格 默认为0") + private Long unlockPrice = 0L; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiInfoApiInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiInfoApiInput.java new file mode 100644 index 0000000..d6873d0 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiInfoApiInput.java @@ -0,0 +1,13 @@ +package com.sonic.frog.domain.input; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Data +public class AiInfoApiInput { + + private Long aiId; + +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserBaseInfoInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserBaseInfoInput.java new file mode 100644 index 0000000..b22a0d4 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserBaseInfoInput.java @@ -0,0 +1,23 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 19:15 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserBaseInfoInput { + + @ApiModelProperty("AI的id") + private Long aiId; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserExtInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserExtInput.java new file mode 100644 index 0000000..df29aba --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserExtInput.java @@ -0,0 +1,74 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 15:38 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserExtInput { + + @ApiModelProperty("人物设定") + @NotBlank + @Length(min = 10 , max = 2000) + private String profile; + + @ApiModelProperty("对话风格") + @NotBlank + @Length(min = 10 , max = 300) + private String dialogueStyle; + + @ApiModelProperty("人物设定 用户输入") + private String userProfile; + + @ApiModelProperty("人物设定 用户输入扩展字段") + private String userProfileExtJson; + + @ApiModelProperty("对话风格 用户输入") + private String userDialogueStyle; + + @ApiModelProperty("对话开场白") + @NotBlank + @Length(min = 10 , max = 150) + private String dialoguePrologue; + + @ApiModelProperty("对话音色code") + @NotBlank + private String dialogueTimbreCode; + + @ApiModelProperty("对话-音高") + @NotBlank + private String dialoguePitch; + + @ApiModelProperty("对话-语速") + @NotBlank + private String dialogueSpeechRate; + + @ApiModelProperty("对话音色url(可能是试听后的url,需要单独保存)") + @NotBlank + private String dialogueTimbreUrl; + + @ApiModelProperty("形象风格code") + @NotBlank + private String imageStyleCode; + + @ApiModelProperty("形象描述") + @NotBlank + @Length(min = 10 , max = 1000) + private String imageDesc; + + @ApiModelProperty("形象参考") + private String imageReferenceUrl; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserGiftListInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserGiftListInput.java new file mode 100644 index 0000000..ff88ece --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserGiftListInput.java @@ -0,0 +1,29 @@ +package com.sonic.frog.domain.input; + +import com.sonic.common.rpc.Page; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 19:21 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserGiftListInput { + + @ApiModelProperty("AI的id") + @NotNull + private Long aiId; + + @NotNull + Page page = new Page<>(1, 10); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserH5InfoInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserH5InfoInput.java new file mode 100644 index 0000000..4744345 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserH5InfoInput.java @@ -0,0 +1,25 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 20:05 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserH5InfoInput { + + @ApiModelProperty("AI的id") + @NotNull + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserImBaseInfoInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserImBaseInfoInput.java new file mode 100644 index 0000000..d51a544 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserImBaseInfoInput.java @@ -0,0 +1,23 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 19:15 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserImBaseInfoInput { + + @ApiModelProperty("AI的id") + private Long aiId; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserLikeOrCancelInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserLikeOrCancelInput.java new file mode 100644 index 0000000..6e32b85 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserLikeOrCancelInput.java @@ -0,0 +1,30 @@ +package com.sonic.frog.domain.input; + +import com.sonic.frog.domain.entity.Liked; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/*** @Author zzhan + * @Description 相册图片点赞、取消点赞 + * @Date 2023/8/30 15:34 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserLikeOrCancelInput { + + @ApiModelProperty(value = "AI Id", required = true) + @NotNull + private Long aiId; + @ApiModelProperty(value = "点赞状态(LIKED已点赞、CANCELED取消点赞)", required = true) + @NotNull + private Liked.LikedStatus likedStatus; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserSeoBaseInfoInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserSeoBaseInfoInput.java new file mode 100644 index 0000000..23933e2 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserSeoBaseInfoInput.java @@ -0,0 +1,25 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 20:05 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserSeoBaseInfoInput { + + @ApiModelProperty("AI的id") + @NotNull + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserStatInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserStatInput.java new file mode 100644 index 0000000..8bb1dfa --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AiUserStatInput.java @@ -0,0 +1,22 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-13 10:46 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserStatInput { + + @ApiModelProperty("AI的id") + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AlbumListInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AlbumListInput.java new file mode 100644 index 0000000..38fdaa3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/AlbumListInput.java @@ -0,0 +1,31 @@ +package com.sonic.frog.domain.input; + +import com.sonic.common.rpc.Page; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 20:26 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AlbumListInput { + + @ApiModelProperty("ai的id") + @NotNull + private Long aiId; + + private Page page = new Page(1, 10); + + @ApiModelProperty("用户ip 内部使用") + private String IpAddress; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BatchAddAlbumInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BatchAddAlbumInput.java new file mode 100644 index 0000000..794939c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BatchAddAlbumInput.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 - 2050 zyp.All Rights Reserved. + * + */ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * Created by mzc + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BatchAddAlbumInput { + + @ApiModelProperty("ai的Id") + private Long aiId; + + @NotNull(message = "Image cannot be empty") + @ApiParam(value = "图片地址", required = true) + private List images; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BatchAddBackgroundInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BatchAddBackgroundInput.java new file mode 100644 index 0000000..c187f51 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BatchAddBackgroundInput.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 - 2050 zyp.All Rights Reserved. + * + */ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * Created by mzc + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BatchAddBackgroundInput { + + @ApiModelProperty("ai的Id") + private Long aiId; + + @NotNull(message = "Image cannot be empty") + @ApiParam(value = "图片信息", required = true) + private List images; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BuyCreateImageCountInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BuyCreateImageCountInput.java new file mode 100644 index 0000000..c9bda26 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BuyCreateImageCountInput.java @@ -0,0 +1,22 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-09-10 17:53 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BuyCreateImageCountInput { + + @ApiModelProperty("购买数量") + private Integer count; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BuyHeartbeatValInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BuyHeartbeatValInput.java new file mode 100644 index 0000000..462ebab --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/BuyHeartbeatValInput.java @@ -0,0 +1,30 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * @description: + * @author: mzc + * @date: 2025-08-21 13:52 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BuyHeartbeatValInput { + + @ApiModelProperty("aiId") + @NotNull + private Long aiId; + + @ApiModelProperty("购买心动值数量") + @NotNull + private BigDecimal heartbeatVal; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ChatBubbleListInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ChatBubbleListInput.java new file mode 100644 index 0000000..72e4771 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ChatBubbleListInput.java @@ -0,0 +1,25 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-08-24 18:37 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChatBubbleListInput { + + @ApiModelProperty("aiId") + @NotNull + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ChatSetInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ChatSetInput.java new file mode 100644 index 0000000..2da3ca7 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ChatSetInput.java @@ -0,0 +1,20 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +/** + * @description: + * @author: mzc + * @date: 2025-08-18 15:57 + **/ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ChatSetInput { + + @ApiModelProperty("ai的Id") + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ClassificationListInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ClassificationListInput.java new file mode 100644 index 0000000..657f60f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ClassificationListInput.java @@ -0,0 +1,54 @@ +package com.sonic.frog.domain.input; + +import com.google.common.collect.Lists; +import com.sonic.frog.enums.AgeTypeEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-09-08 10:29 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ClassificationListInput { + @ApiModelProperty("角色code列表") + private List roleCodeList; + + @ApiModelProperty("情感性格code") + private List characterCodeList; + + @ApiModelProperty("标签code列表") + private List tagCodeList; + + @ApiModelProperty("需要排除的aiId列表") + private List exList; + + @ApiModelProperty("页码") + private int pn; + + @ApiModelProperty("每页大小") + @NotNull + private int ps = 20; + + @ApiModelProperty("性别列表") + private List sexList; + + @ApiModelProperty("年龄列表") + private List ageList; + + @ApiModelProperty("年龄范围列表(内部使用)") + private List ageRangeList; + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/CreateEditAiUserInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/CreateEditAiUserInput.java new file mode 100644 index 0000000..27796c5 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/CreateEditAiUserInput.java @@ -0,0 +1,84 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Date; + +/** + * @description: + * @author: mzc + * @date: 2025-07-10 15:54 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateEditAiUserInput { + + @ApiModelProperty("AI的id,编辑时才会传") + private Long aiId; + + /** + * AI基础信息 + */ + @ApiModelProperty("昵称") + @NotBlank + @Length(min = 2 , max = 20) + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + @NotNull + private Integer sex; + + @ApiModelProperty("头像") + @NotBlank + private String headImg; + + @ApiModelProperty("出生日期") + @NotNull + private LocalDateTime birthday; + + @ApiModelProperty("角色code") + @NotBlank + private String roleCode; + + @ApiModelProperty("性格code") + @NotBlank + private String characterCode; + + @ApiModelProperty("标签code") + @NotBlank + private String tagCode; + + @ApiModelProperty("简介") + @NotBlank + @Length(min = 10 , max = 300) + private String introduction; + + @ApiModelProperty("权限 1: 公开 2:私密") + @NotNull + private Integer permission; + + @ApiModelProperty("形象图") + @NotBlank + private String imageUrl; + + @ApiModelProperty("形象图-宽") + private String imageWidth; + + @ApiModelProperty("形象图-高") + private String imageHeight; + + /** + * 对应Ai用户扩展表 + */ + private AiUserExtInput aiUserExt; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/DelAiUserInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/DelAiUserInput.java new file mode 100644 index 0000000..baddb26 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/DelAiUserInput.java @@ -0,0 +1,22 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 20:01 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DelAiUserInput { + + @ApiModelProperty("AI的id") + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/DelAlbumInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/DelAlbumInput.java new file mode 100644 index 0000000..9399d2e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/DelAlbumInput.java @@ -0,0 +1,31 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @Author zzhan + * @Description 删除相册数据 + * @Date 2023/8/30 13:57 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DelAlbumInput { + + @ApiModelProperty("用户ID【操作AI用户时使用】") + private Long userId; + + @ApiParam(value = "图片ID", required = true) + @NotNull(message = "{id.not.null}") + Long albumId; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/DelBackgroundInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/DelBackgroundInput.java new file mode 100644 index 0000000..561ac58 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/DelBackgroundInput.java @@ -0,0 +1,16 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * @description: + * @author: mzc + * @date: 2025-08-19 11:05 + **/ +@Data +public class DelBackgroundInput { + + @ApiModelProperty("背景id") + private Long backgroundId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/EditAiHeadImgInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/EditAiHeadImgInput.java new file mode 100644 index 0000000..970ef1f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/EditAiHeadImgInput.java @@ -0,0 +1,28 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.*; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-09-16 15:50 + **/ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EditAiHeadImgInput { + + @ApiModelProperty("ai的Id") + @NotNull + private Long aiId; + + @ApiParam(value = "头像") + @NotNull + private String userHead; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GetAiUserInfoInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GetAiUserInfoInput.java new file mode 100644 index 0000000..1df2c93 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GetAiUserInfoInput.java @@ -0,0 +1,21 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:50 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GetAiUserInfoInput { + @ApiModelProperty("aiId") + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GetRandomLockImageInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GetRandomLockImageInput.java new file mode 100644 index 0000000..720e97d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GetRandomLockImageInput.java @@ -0,0 +1,18 @@ +package com.sonic.frog.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GetRandomLockImageInput { + + private Long userId; + + private Long aiId; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GetTotalDeductionAmountInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GetTotalDeductionAmountInput.java new file mode 100644 index 0000000..5d11d05 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GetTotalDeductionAmountInput.java @@ -0,0 +1,22 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-09-18 16:23 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GetTotalDeductionAmountInput { + + @ApiModelProperty("用户id") + private Long userId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GiftDictListInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GiftDictListInput.java new file mode 100644 index 0000000..741c534 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/GiftDictListInput.java @@ -0,0 +1,24 @@ +package com.sonic.frog.domain.input; + +import com.sonic.common.rpc.Page; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-08-15 16:06 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GiftDictListInput { + + @NotNull + Page page = new Page<>(1, 10); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HeartbeatLevelInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HeartbeatLevelInput.java new file mode 100644 index 0000000..a4d224c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HeartbeatLevelInput.java @@ -0,0 +1,25 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-08-18 13:57 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HeartbeatLevelInput { + + @ApiModelProperty("AI的id") + @NotNull + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HeartbeatRelationListInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HeartbeatRelationListInput.java new file mode 100644 index 0000000..8bbad73 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HeartbeatRelationListInput.java @@ -0,0 +1,31 @@ +package com.sonic.frog.domain.input; + +import com.sonic.common.rpc.Page; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-08-25 11:40 + **/ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class HeartbeatRelationListInput { + + @ApiModelProperty("搜索昵称") + private String nickname; + + @NotNull + Page page = new Page<>(1, 10); + + @ApiModelProperty("内部使用") + private Long userId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HeartbeatRelationSwitchInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HeartbeatRelationSwitchInput.java new file mode 100644 index 0000000..5097f9f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HeartbeatRelationSwitchInput.java @@ -0,0 +1,29 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-08-21 15:15 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HeartbeatRelationSwitchInput { + + @ApiModelProperty("ai的Id") + @NotNull + private Long aiId; + + @ApiModelProperty("关系显示开关 默认关闭 0:关闭 1:打开") + @NotNull + private Boolean isShow; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HomeRecommendInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HomeRecommendInput.java new file mode 100644 index 0000000..f041e0b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/HomeRecommendInput.java @@ -0,0 +1,43 @@ +package com.sonic.frog.domain.input; + +import com.google.common.collect.Lists; +import com.sonic.frog.enums.AgeTypeEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class HomeRecommendInput { + + @ApiModelProperty("页码") + private int pn; + + @ApiModelProperty("每页大小") + private int ps = 20; + + @ApiModelProperty("需要排除的ID列表") + private List exList = Lists.newArrayList(); + + @ApiModelProperty("是否随机") + private Boolean random; + + @ApiModelProperty("类别:多选") + private List roleCodeList; + + @ApiModelProperty("性别列表 多选") + private List sexList = Lists.newArrayList(); + + @ApiModelProperty("年龄列表 多选") + private List ageList = Lists.newArrayList(); + + @ApiModelProperty("年龄范围列表(内部使用)") + private List ageRangeList = Lists.newArrayList(); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/LikeOrCancelPicInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/LikeOrCancelPicInput.java new file mode 100644 index 0000000..2f484fc --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/LikeOrCancelPicInput.java @@ -0,0 +1,30 @@ +package com.sonic.frog.domain.input; + +import com.sonic.frog.domain.entity.Liked; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/*** @Author zzhan + * @Description 相册图片点赞、取消点赞 + * @Date 2023/8/30 15:34 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LikeOrCancelPicInput { + + @ApiModelProperty(value = "相册图片 Id", required = true) + @NotNull + private Long albumId; + @ApiModelProperty(value = "点赞状态(LIKED已点赞、CANCELED取消点赞)", required = true) + @NotNull + private Liked.LikedStatus likedStatus; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MeetInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MeetInput.java new file mode 100644 index 0000000..e511a0f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MeetInput.java @@ -0,0 +1,17 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MeetInput { + + @ApiModelProperty("AI的ID") + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MeetSdInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MeetSdInput.java new file mode 100644 index 0000000..4acc396 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MeetSdInput.java @@ -0,0 +1,20 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MeetSdInput { + + @ApiModelProperty("滑动的AI的ID") + private Long aiId; + + @ApiModelProperty("是否喜欢") + private Boolean lk = false; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MeetUnlockInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MeetUnlockInput.java new file mode 100644 index 0000000..9d8a687 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MeetUnlockInput.java @@ -0,0 +1,17 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MeetUnlockInput { + + @ApiModelProperty("AI的ID") + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MockInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MockInput.java new file mode 100644 index 0000000..6dbc557 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/MockInput.java @@ -0,0 +1,20 @@ +package com.sonic.frog.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class MockInput { + + private Long aiId; + + private List aiIdList; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SendDialoguePrologueMessageInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SendDialoguePrologueMessageInput.java new file mode 100644 index 0000000..83eab45 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SendDialoguePrologueMessageInput.java @@ -0,0 +1,22 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-08-29 17:53 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SendDialoguePrologueMessageInput { + + @ApiModelProperty("AI的id") + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SendGiftInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SendGiftInput.java new file mode 100644 index 0000000..daa42c9 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SendGiftInput.java @@ -0,0 +1,32 @@ +package com.sonic.frog.domain.input; + +import com.sonic.frog.enums.SendGiftSceneEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-08-22 10:08 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SendGiftInput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("礼物id") + private Long giftId; + + @ApiModelProperty("礼物数量") + private Integer num; + + @ApiModelProperty("发送礼物场景") + private SendGiftSceneEnum scene = SendGiftSceneEnum.IM; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetAlbumUnlockPriceInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetAlbumUnlockPriceInput.java new file mode 100644 index 0000000..6dafcc3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetAlbumUnlockPriceInput.java @@ -0,0 +1,34 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @Author zzhan + * @Description 删除相册数据 + * @Date 2023/8/30 13:57 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SetAlbumUnlockPriceInput { + + @ApiModelProperty("ai用户id") + @NotNull + private Long aiId; + + @ApiParam(value = "图片ID", required = true) + @NotNull(message = "{id.not.null}") + private Long albumId; + + @ApiModelProperty("解锁价格 默认为0") + private Long unlockPrice = 0L; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetBackgroundInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetBackgroundInput.java new file mode 100644 index 0000000..ce9ed6f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetBackgroundInput.java @@ -0,0 +1,25 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-08-18 18:16 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SetBackgroundInput { + + @ApiModelProperty("ai的Id") + private Long aiId; + + @ApiModelProperty("背景图片id") + private Long backgroundId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetChatBubbleInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetChatBubbleInput.java new file mode 100644 index 0000000..2bd60f1 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetChatBubbleInput.java @@ -0,0 +1,29 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-08-18 17:34 + **/ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SetChatBubbleInput { + + @ApiModelProperty("ai的Id") + @NotNull + private Long aiId; + + @ApiModelProperty("对应的气泡code") + @NotNull + private String code; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetChatModelInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetChatModelInput.java new file mode 100644 index 0000000..e603c9b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetChatModelInput.java @@ -0,0 +1,19 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * @description: + * @author: mzc + * @date: 2025-08-18 17:33 + **/ +@Data +public class SetChatModelInput { + + @ApiModelProperty("ai的Id") + private Long aiId; + + @ApiModelProperty("对应的模型code") + private String code; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetDefaultAlbumInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetDefaultAlbumInput.java new file mode 100644 index 0000000..76ae37d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetDefaultAlbumInput.java @@ -0,0 +1,32 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @Author zzhan + * @Description 删除相册数据 + * @Date 2023/8/30 13:57 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SetDefaultAlbumInput { + + @ApiModelProperty("ai用户id") + @NotNull + private Long aiId; + + @ApiParam(value = "图片ID", required = true) + @NotNull(message = "{id.not.null}") + Long albumId; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetIsAutoPlayVoiceInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetIsAutoPlayVoiceInput.java new file mode 100644 index 0000000..0d3b55a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetIsAutoPlayVoiceInput.java @@ -0,0 +1,28 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-08-24 11:01 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SetIsAutoPlayVoiceInput { + @ApiModelProperty("ai的Id") + @NotNull + private Long aiId; + + @ApiModelProperty("是否自动播放语音") + @NotNull + private Boolean isAutoPlayVoice; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetMyChatSettingInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetMyChatSettingInput.java new file mode 100644 index 0000000..7915922 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SetMyChatSettingInput.java @@ -0,0 +1,37 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-08-18 16:27 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SetMyChatSettingInput { + + @ApiModelProperty("ai的Id") + @NotNull + private Long aiId; + + @NotEmpty + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("我是谁") + private String whoAmI; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SubMemberGiftUserCreateCountInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SubMemberGiftUserCreateCountInput.java new file mode 100644 index 0000000..9796bd8 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/SubMemberGiftUserCreateCountInput.java @@ -0,0 +1,30 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-10-21 18:30 + **/ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SubMemberGiftUserCreateCountInput { + + @ApiModelProperty("用户id") + private Long userId; + + @ApiModelProperty("订阅或续订会员时间") + private LocalDateTime startTime; + + @ApiModelProperty("会员过期时间") + private LocalDateTime expireTime; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ThirdLoginOrRegisterInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ThirdLoginOrRegisterInput.java new file mode 100644 index 0000000..aba3043 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ThirdLoginOrRegisterInput.java @@ -0,0 +1,48 @@ +package com.sonic.frog.domain.input; + +import com.sonic.bear.lib.enums.ThirdTypeEnum; +import com.sonic.common.auth.domains.AppClientEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * @Author code + * @Description 登录或注册验证的入参 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ThirdLoginOrRegisterInput { + + @NotEmpty + @ApiModelProperty(value = "三方授权码[discord、twitch传的是code码,中间需要根据这个code码来获取授权token]", required = true) + private String thirdToken; + + @NotNull + @ApiModelProperty(value = "第三方账号类型, DISCORD、GOOGLE、APPLE", required = true) + private ThirdTypeEnum thirdType; + + @NotNull + @ApiModelProperty(value = "登陆端(WEB、IOS、ANDROID)", required = true) + private AppClientEnum appClient; + + @NotEmpty + @ApiModelProperty(value = "手机设备唯一码", required = true) + private String deviceCode; + + @ApiModelProperty(value = "【内部】设置及使用") + private String ip; + + @ApiModelProperty(value = "【内部】设置及使用") + private String userAgent; + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/TokenCheckInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/TokenCheckInput.java new file mode 100644 index 0000000..5f509ef --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/TokenCheckInput.java @@ -0,0 +1,17 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TokenCheckInput { + + @ApiModelProperty("授权Token") + private String token; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UnlockAlbumImgInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UnlockAlbumImgInput.java new file mode 100644 index 0000000..533af25 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UnlockAlbumImgInput.java @@ -0,0 +1,28 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-08-22 11:15 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UnlockAlbumImgInput { + + @ApiModelProperty("aiId") + private Long aiId; + + @ApiModelProperty("相册图片id") + private Long albumId; + + @ApiModelProperty("消息ID") + private Long messageServerId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UnlockLikeYouInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UnlockLikeYouInput.java new file mode 100644 index 0000000..6ad93cf --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UnlockLikeYouInput.java @@ -0,0 +1,32 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-09-10 17:59 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UnlockLikeYouInput { + + @ApiModelProperty("aiId") + @NotNull + private Long aiId; + + @ApiModelProperty("相册图片id") + @NotNull + private Long albumId; + + @ApiModelProperty("用户ip(内部使用)") + private String ipAddress; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UpdateIsDelChattedInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UpdateIsDelChattedInput.java new file mode 100644 index 0000000..4bdd375 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UpdateIsDelChattedInput.java @@ -0,0 +1,20 @@ +package com.sonic.frog.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UpdateIsDelChattedInput { + + private Long userId; + + private List aiIdList; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UseUserCreateCountInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UseUserCreateCountInput.java new file mode 100644 index 0000000..62311cc --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UseUserCreateCountInput.java @@ -0,0 +1,22 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-09-17 11:17 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UseUserCreateCountInput { + + @ApiModelProperty("用户Id") + private Long userId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UserIdInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UserIdInput.java new file mode 100644 index 0000000..63d6fc2 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/UserIdInput.java @@ -0,0 +1,14 @@ +package com.sonic.frog.domain.input; + +import lombok.Data; + +import java.util.List; + +@Data +public class UserIdInput { + + private Long userId; + + private List userIdList; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ViewUnlockAlbumImgInput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ViewUnlockAlbumImgInput.java new file mode 100644 index 0000000..b0de89d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/input/ViewUnlockAlbumImgInput.java @@ -0,0 +1,34 @@ +package com.sonic.frog.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-08-22 11:15 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ViewUnlockAlbumImgInput { + + @ApiModelProperty("aiId") + private Long aiId; + + @ApiModelProperty("相册图片id") + @NotNull + private Long albumId; + + @ApiModelProperty("消息ID") + private Long messageServerId; + + @ApiModelProperty("用户ip 内部使用") + private String IpAddress; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AdvertiseOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AdvertiseOutput.java new file mode 100644 index 0000000..80e842d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AdvertiseOutput.java @@ -0,0 +1,74 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-09-08 16:06 + **/ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AdvertiseOutput { + + /** + * 广告名称 + */ + @ApiModelProperty("name") + private String name; + + /** + * 广告配图 + */ + @ApiModelProperty("icon") + private String icon; + + /** + * 跳转连接 + */ + @ApiModelProperty("jump_link") + private String jumpLink; + + /** + * 展示开始时间 + */ + @ApiModelProperty("show_start_time") + private LocalDateTime showStartTime; + + /** + * 展示结束时间 + */ + @ApiModelProperty("show_end_time") + private LocalDateTime showEndTime; + + /** + * 扩展字段 + */ + @ApiModelProperty("ext") + private String ext; + + /** + * 排序 + */ + @ApiModelProperty("sort") + private Integer sort; + + /** + * 使用端点(WEB/ANDROID/IOS) + */ + @ApiModelProperty("endpoint") + private String endpoint; + + /** + * 是否弹窗(1.是,0.否) + */ + @ApiModelProperty("is_global") + private Integer isGlobal; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiAlbumDetailOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiAlbumDetailOutput.java new file mode 100644 index 0000000..6a1daf5 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiAlbumDetailOutput.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018 - 2050 zyp.All Rights Reserved. + * + */ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +/** + * Created by xi.he on 2020/2/5 + */ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AiAlbumDetailOutput { + + @ApiModelProperty("ai的ID") + private Long aiId; + + @ApiModelProperty("图片id") + private Long albumId; + + @ApiModelProperty("图片宽") + private String width; + + @ApiModelProperty("图片高") + private String height; + + @ApiModelProperty("模糊图片1") + private String img1; + + @ApiModelProperty("模糊图片2") + private String img2; + + @ApiModelProperty("模糊图片3") + private String img3; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiCarouselListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiCarouselListOutput.java new file mode 100644 index 0000000..8edd008 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiCarouselListOutput.java @@ -0,0 +1,63 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * ai轮播列表输出 + * + * @description: + * @author: mzc + * @date: 2025-11-05 16:15 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AiCarouselListOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("ai所属用户id") + private Long userId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("主页头图") + private String homeImageUrl; + + @ApiModelProperty("点赞数") + private Integer likedCount; + + @ApiModelProperty("是否点赞过") + private Boolean liked; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiChatInfoOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiChatInfoOutput.java new file mode 100644 index 0000000..02764e3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiChatInfoOutput.java @@ -0,0 +1,53 @@ +package com.sonic.frog.domain.output; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.sonic.frog.domain.entity.ImageStyleDict; +import com.sonic.frog.domain.entity.TimbreDict; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:22 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiChatInfoOutput { + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("我是谁") + private String whoAmI; + + @ApiModelProperty("相识天数") + private Integer dayCount; + + @ApiModelProperty("是否通过meet相识") + private Boolean meet; + + @ApiModelProperty("心动等级") + private HeartbeatLevelEnum heartbeatLevel; + + @ApiModelProperty("关系阶段") + private String relationStage; + + @ApiModelProperty("解锁的心动等级列表") + private List unlockHearbeatLevelList; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiChatRankOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiChatRankOutput.java new file mode 100644 index 0000000..05d3037 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiChatRankOutput.java @@ -0,0 +1,56 @@ +package com.sonic.frog.domain.output; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiChatRankOutput { + + @ApiModelProperty(value = "排名编号") + private Integer rankNo; + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("主页头图") + private String homeImageUrl; + + @ApiModelProperty("聊天次数") + private Integer chatNum; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiDictOut.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiDictOut.java new file mode 100644 index 0000000..e53a1ef --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiDictOut.java @@ -0,0 +1,38 @@ +package com.sonic.frog.domain.output; + +import com.sonic.frog.domain.entity.ImageStyleDict; +import com.sonic.frog.domain.entity.TimbreDict; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:22 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiDictOut { + + @ApiModelProperty("角色字典") + private List roleDictList; + + @ApiModelProperty("性格字典") + private List characterDictList; + + @ApiModelProperty("标签字典") + private List tagDictList; + + @ApiModelProperty("所有形象风格字典片列表") + private List imageStyleDictList; + + @ApiModelProperty("所有音色列表") + private List timbreDictList; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiGiftRankOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiGiftRankOutput.java new file mode 100644 index 0000000..40ccaa5 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiGiftRankOutput.java @@ -0,0 +1,56 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiGiftRankOutput { + + @ApiModelProperty(value = "排名编号") + private Integer rankNo; + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("主页头图") + private String homeImageUrl; + + @ApiModelProperty("礼物") + private Long giftCoinNum; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiHeartbeatRankOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiHeartbeatRankOutput.java new file mode 100644 index 0000000..370206a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiHeartbeatRankOutput.java @@ -0,0 +1,57 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiHeartbeatRankOutput { + + @ApiModelProperty(value = "排名编号") + private Integer rankNo; + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("主页头图") + private String homeImageUrl; + + @ApiModelProperty("心动总分值") + private BigDecimal heartbeatValTotal; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiInfoApiOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiInfoApiOutput.java new file mode 100644 index 0000000..8ba50ae --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiInfoApiOutput.java @@ -0,0 +1,67 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@Data +public class AiInfoApiOutput { + + + @ApiModelProperty("ai所属用户id") + private Long userId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色code 对应ai_dict表中code") + private String roleCode; + + @ApiModelProperty("角色描述") + private String roleName; + + @ApiModelProperty("性格code 对应ai_dict表中code") + private String characterCode; + + @ApiModelProperty("性格描述") + private String characterName; + + @ApiModelProperty("标签code 对应ai_dict表中code") + private String tagCode; + + @ApiModelProperty("标签描述") + private String tagName; + + @ApiModelProperty("人物设定") + private String profile; + + @ApiModelProperty("人物设定 扩展字段") + private String userProfileExtJson; + + @ApiModelProperty("对话风格") + private String dialogueStyle; + + @ApiModelProperty("对话开场白") + private String dialoguePrologue; + + @ApiModelProperty("基图-首次创建AI时选择的形象图") + private String baseImageUrl; + + @ApiModelProperty("语音类型 第三方") + private String voiceType; + + @ApiModelProperty("对话-音高") + private String dialoguePitch; + + @ApiModelProperty("对话-语速") + private String dialogueSpeechRate; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserBaseListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserBaseListOutput.java new file mode 100644 index 0000000..ab948ab --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserBaseListOutput.java @@ -0,0 +1,53 @@ +package com.sonic.frog.domain.output; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Date; + +@Data +public class AiUserBaseListOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("AI用户对外展示ID") + @TableField("id_card") + private String idCard; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("权限 1: 公开 2:私密") + private Integer permission; + + @ApiModelProperty("主页头图") + private String homeImageUrl; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserBaseOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserBaseOutput.java new file mode 100644 index 0000000..a74c03b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserBaseOutput.java @@ -0,0 +1,61 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 10:53 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserBaseOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("ai所属用户id") + private Long userId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("主页头图") + private String homeImageUrl; + + @ApiModelProperty("点赞数") + private Integer likedCount; + + @ApiModelProperty("是否点赞过") + private Boolean liked; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserExtOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserExtOutput.java new file mode 100644 index 0000000..004210f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserExtOutput.java @@ -0,0 +1,62 @@ +package com.sonic.frog.domain.output; + +import com.sonic.frog.domain.entity.ImageStyleDict; +import com.sonic.frog.domain.entity.TimbreDict; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 15:38 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserExtOutput { + + @ApiModelProperty("人物设定") + private String profile; + + @ApiModelProperty("对话风格") + private String dialogueStyle; + + @ApiModelProperty("人物设定 用户输入") + private String userProfile; + + @ApiModelProperty("对话风格 用户输入") + private String userDialogueStyle; + + @ApiModelProperty("对话开场白") + private String dialoguePrologue; + + @ApiModelProperty("对话音色code") + private String dialogueTimbreCode; + + @ApiModelProperty("对话音色字典信息") + private TimbreDict timbreDict; + + @ApiModelProperty("对话-音高") + private String dialoguePitch; + + @ApiModelProperty("对话-语速") + private String dialogueSpeechRate; + + @ApiModelProperty("形象风格code") + private String imageStyleCode; + + @ApiModelProperty("形象风格字典信息") + private ImageStyleDict imageStyleDict; + + @ApiModelProperty("形象描述") + private String imageDesc; + + @ApiModelProperty("形象参考") + private String imageReferenceUrl; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserGiftListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserGiftListOutput.java new file mode 100644 index 0000000..36e730b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserGiftListOutput.java @@ -0,0 +1,31 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 19:22 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserGiftListOutput { + + @ApiModelProperty("礼物id") + private Integer id; + + @ApiModelProperty("礼物名称") + private String name; + + @ApiModelProperty("礼物icon") + private String icon; + + @ApiModelProperty("获得礼物数量") + private Integer getNum; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserH5Output.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserH5Output.java new file mode 100644 index 0000000..3f06930 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserH5Output.java @@ -0,0 +1,54 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 20:05 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserH5Output { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("背景图") + private String backgroundImg; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("对话开场白") + private String dialoguePrologue; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserHeartbeatRelationOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserHeartbeatRelationOutput.java new file mode 100644 index 0000000..46da09f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserHeartbeatRelationOutput.java @@ -0,0 +1,52 @@ +package com.sonic.frog.domain.output; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * @description: + * @author: mzc + * @date: 2025-08-15 17:04 + **/ +@Data +public class AiUserHeartbeatRelationOutput { + + @ApiModelProperty("关系显示开关 默认关闭 0:关闭 1:打开") + private Boolean isShow = true; + + @ApiModelProperty("AI头像") + private String aiHeadImg; + + @ApiModelProperty("用户头像") + private String userHeadImg; + + @ApiModelProperty("心动等级") + private HeartbeatLevelEnum heartbeatLevel; + + @ApiModelProperty("心动等级数字") + private Integer heartbeatLevelNum; + + @ApiModelProperty("心动值") + private BigDecimal heartbeatVal; + + @ApiModelProperty("心动等级名称") + private String heartbeatLevelName; + + @ApiModelProperty("相识天数") + private Integer dayCount; + + @ApiModelProperty("心动分") + private BigDecimal heartbeatScore; + + @ApiModelProperty("已扣减心动值") + private BigDecimal subtractHeartbeatVal; + + @ApiModelProperty("心动值单价") + private Long price; +} + + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserImBaseInfoOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserImBaseInfoOutput.java new file mode 100644 index 0000000..92fa7c6 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserImBaseInfoOutput.java @@ -0,0 +1,97 @@ +package com.sonic.frog.domain.output; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 10:53 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserImBaseInfoOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("ai所属用户id") + private Long userId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("im聊天背景图") + private String backgroundImg; + + @ApiModelProperty("是否是默认背景图片") + private Boolean isDefaultBackground; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; + + @ApiModelProperty("对话开场白") + private String dialoguePrologue; + + @ApiModelProperty("对话开场白-语音") + private String dialoguePrologueVoice; + + @ApiModelProperty("用户与AI的心动关系") + private AiUserHeartbeatRelationOutput aiUserHeartbeatRelation; + + @ApiModelProperty("聊天气泡") + private ChatBubbleOutput chatBubble; + + @ApiModelProperty("自动播放语音开关 1:开 0:关") + private Integer isAutoPlayVoice; + + @ApiModelProperty("是否是会员") + private Boolean isMember; + + @ApiModelProperty("语音类型 第三方") + private String voiceType; + + @ApiModelProperty("对话-音高") + private String dialoguePitch; + + @ApiModelProperty("对话-语速") + private String dialogueSpeechRate; + + @ApiModelProperty("是否点赞过") + private Boolean liked; + + @ApiModelProperty("是否已聊天过") + private Boolean isHaveChatted; + + @ApiModelProperty("是否删除聊天消息 1:是 0:否") + private Boolean isDelChatted; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserInfoOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserInfoOutput.java new file mode 100644 index 0000000..c5a9a12 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserInfoOutput.java @@ -0,0 +1,75 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Date; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 10:52 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserInfoOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + /** + * AI基础信息 + */ + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + @NotNull + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色code") + private String roleCode; + + @ApiModelProperty("角色") + private String role; + + @ApiModelProperty("性格code") + private String characterCode; + + @ApiModelProperty("性格") + private String character; + + @ApiModelProperty("标签code") + private String tagCode; + + @ApiModelProperty("标签") + private String tag; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("权限 1: 公开 2:私密") + private Integer permission; + + @ApiModelProperty("形象图") + private String imageUrl; + + /** + * 对应Ai用户扩展表 + */ + private AiUserExtOutput aiUserExt; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserSeoBaseInfoOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserSeoBaseInfoOutput.java new file mode 100644 index 0000000..28b1a12 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserSeoBaseInfoOutput.java @@ -0,0 +1,28 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 20:05 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserSeoBaseInfoOutput { + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("主图") + private String homeImageUrl; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserStatOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserStatOutput.java new file mode 100644 index 0000000..40e1ff9 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserStatOutput.java @@ -0,0 +1,36 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: Ai用户统计 + * @author: mzc + * @date: 2025-07-13 10:44 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiUserStatOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; + + @ApiModelProperty("聊天数") + private Integer chatNum; + + @ApiModelProperty("产生过对话的人数") + private Integer conversationNum; + + @ApiModelProperty("赚取的Crush Coin数") + private Long coinNum; + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserTargetListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserTargetListOutput.java new file mode 100644 index 0000000..c16cd74 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/AiUserTargetListOutput.java @@ -0,0 +1,50 @@ +package com.sonic.frog.domain.output; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Date; + +@Data +public class AiUserTargetListOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("AI用户对外展示ID") + @TableField("id_card") + private String idCard; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("主页头图") + private String homeImageUrl; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/BackgroundImgListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/BackgroundImgListOutput.java new file mode 100644 index 0000000..566ad97 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/BackgroundImgListOutput.java @@ -0,0 +1,37 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-08-18 18:11 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BackgroundImgListOutput { + + @ApiModelProperty("背景图片id") + private Long backgroundId; + + @ApiModelProperty("图片地址") + private String imgUrl; + + @ApiModelProperty("图片宽") + private String width; + + @ApiModelProperty("图片高") + private String height; + + @ApiModelProperty("是否默认图片") + private Boolean isDefault; + + @ApiModelProperty("是否选中图片") + private Boolean isSelected; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/BatchAddAlbumOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/BatchAddAlbumOutput.java new file mode 100644 index 0000000..6302a02 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/BatchAddAlbumOutput.java @@ -0,0 +1,24 @@ +package com.sonic.frog.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2022-10-25 19:55 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class BatchAddAlbumOutput { + /** + * 上传后返回主键id列表 + */ + private List ids; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/BatchAddBackgroundOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/BatchAddBackgroundOutput.java new file mode 100644 index 0000000..bc6a4f3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/BatchAddBackgroundOutput.java @@ -0,0 +1,24 @@ +package com.sonic.frog.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2022-10-25 19:55 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class BatchAddBackgroundOutput { + /** + * 上传后返回主键id列表 + */ + private List ids; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatBubbleListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatBubbleListOutput.java new file mode 100644 index 0000000..f7830f0 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatBubbleListOutput.java @@ -0,0 +1,50 @@ +package com.sonic.frog.domain.output; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.sonic.frog.enums.BubbleUnlockTypeEnum; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +/** + * @description: + * @author: mzc + * @date: 2025-08-24 18:37 + **/ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ChatBubbleListOutput { + + @ApiModelProperty("id") + private Integer id; + + @ApiModelProperty("code") + private String code; + + @ApiModelProperty("名称") + private String name; + + @ApiModelProperty("颜色") + private String color; + + @ApiModelProperty("图片url") + private String imgUrl; + + @ApiModelProperty("web端图片url") + private String webImgUrl; + + @ApiModelProperty("解锁类型 MEMBER:会员 HEARTBEAT_LEVEL:心动等级") + private BubbleUnlockTypeEnum unlockType; + + @ApiModelProperty("解锁心动等级 类型为HEARTBEAT_LEVEL时才有用") + private HeartbeatLevelEnum unlockHeartbeatLevel; + + @ApiModelProperty("当前用户是否解锁 false:未解锁,true:解锁") + private Boolean isUnlock; + + @ApiModelProperty("是否默认") + private Boolean isDefault; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatBubbleOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatBubbleOutput.java new file mode 100644 index 0000000..70cdab7 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatBubbleOutput.java @@ -0,0 +1,37 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-08-18 15:00 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChatBubbleOutput { + + @ApiModelProperty("code") + private String code; + + @ApiModelProperty("名称") + private String name; + + @ApiModelProperty("颜色") + private String color; + + @ApiModelProperty("图片url") + private String imgUrl; + + @ApiModelProperty("web端图片url") + private String webImgUrl; + + @ApiModelProperty("是否默认") + private Boolean isDefault; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatModelDictOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatModelDictOutput.java new file mode 100644 index 0000000..cf7fdb1 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatModelDictOutput.java @@ -0,0 +1,41 @@ +package com.sonic.frog.domain.output; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-08-15 16:25 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ChatModelDictOutput { + + @ApiModelProperty("对话模型code") + private String code; + + @ApiModelProperty("对话模型名称") + private String name; + + @ApiModelProperty("对话模型描述") + private String description; + + @ApiModelProperty("文本价格") + private Long textPrice; + + @ApiModelProperty("语音价格") + private Long voicePrice; + + @ApiModelProperty("语音聊天价格") + private Long voiceChatPrice; + + @ApiModelProperty("问号图标内容") + private String questionMark; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatSetOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatSetOutput.java new file mode 100644 index 0000000..64fc11d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ChatSetOutput.java @@ -0,0 +1,55 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-08-18 15:58 + **/ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ChatSetOutput { + + @ApiModelProperty("ai的Id") + private Long aiId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("我是谁") + private String whoAmI; + + @ApiModelProperty("对话模型code") + private String modelCode; + + @ApiModelProperty("对话模型名称") + private String modelName; + + @ApiModelProperty("聊天气泡code") + private String bubbleCode; + + @ApiModelProperty("聊天气泡名称") + private String bubbleName; + + @ApiModelProperty("聊天背景图片") + private String backgroundImg; + + @ApiModelProperty("是否是默认背景图片") + private Boolean isDefaultBackground; + + @ApiModelProperty("自动播放语音开关 1:开 0:关") + private Integer isAutoPlayVoice; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ClassificationListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ClassificationListOutput.java new file mode 100644 index 0000000..5222b22 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ClassificationListOutput.java @@ -0,0 +1,55 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-09-08 10:33 + **/ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ClassificationListOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("ai所属用户id") + private Long userId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("主页头图") + private String homeImageUrl; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/CreateEditAiUserOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/CreateEditAiUserOutput.java new file mode 100644 index 0000000..ad261ba --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/CreateEditAiUserOutput.java @@ -0,0 +1,22 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-07-30 15:09 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CreateEditAiUserOutput { + + @ApiModelProperty("AI的id") + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/DictOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/DictOutput.java new file mode 100644 index 0000000..e093c10 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/DictOutput.java @@ -0,0 +1,30 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-07-11 11:26 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DictOutput { + + @ApiModelProperty("字典code") + private String code; + + @ApiModelProperty("字典名称") + private String name; + + @ApiModelProperty("字典子级") + private List childDictList; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ExploreInfoOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ExploreInfoOutput.java new file mode 100644 index 0000000..299d66e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ExploreInfoOutput.java @@ -0,0 +1,32 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-09-08 15:55 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExploreInfoOutput { + @ApiModelProperty("广告列表") + private List advertiseList; + + @ApiModelProperty("AI总心动值榜单top3") + private List aiChatRankTop3List; + + @ApiModelProperty("AI总心动值榜单top3") + private List aiHeartbeatRankTop3List; + + @ApiModelProperty("AI总心动值榜单top3") + private List aiGiftRankTop3List; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/GiftDictListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/GiftDictListOutput.java new file mode 100644 index 0000000..08140d2 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/GiftDictListOutput.java @@ -0,0 +1,49 @@ +package com.sonic.frog.domain.output; + +import com.sonic.frog.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * @description: + * @author: mzc + * @date: 2025-08-15 16:07 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GiftDictListOutput { + + @ApiModelProperty("礼物id") + private Integer id; + + @ApiModelProperty("礼物名称") + private String name; + + @ApiModelProperty("礼物单价 对应Coin数量") + private Long price; + + @ApiModelProperty("礼物icon") + private String icon; + + @ApiModelProperty("排序") + private Integer sort; + + @ApiModelProperty("礼物说明") + private String desc; + + @ApiModelProperty("发送该礼物需要的心动等级") + private HeartbeatLevelEnum heartbeatLevel; + + @ApiModelProperty("心动等级最低心动值") + private BigDecimal startVal; + + @ApiModelProperty("是否会员礼物") + private Boolean isMemberGift; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HeartbeatLevelDictOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HeartbeatLevelDictOutput.java new file mode 100644 index 0000000..0000561 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HeartbeatLevelDictOutput.java @@ -0,0 +1,34 @@ +package com.sonic.frog.domain.output; + +import com.sonic.frog.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * @description: 心动等级字典 + * @author: mzc + * @date: 2025-08-18 15:28 + **/ +@Data +public class HeartbeatLevelDictOutput { + + @ApiModelProperty("心动等级code") + private HeartbeatLevelEnum code; + + @ApiModelProperty("心动等级数字") + private Integer num; + + @ApiModelProperty("心动等级名称") + private String name; + + @ApiModelProperty("心动等级图标") + private String imgUrl; + + @ApiModelProperty("用户是否解锁") + private Boolean isUnlock; + + @ApiModelProperty("解锁当前等级的心动值开始值") + private BigDecimal startVal; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HeartbeatLevelOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HeartbeatLevelOutput.java new file mode 100644 index 0000000..d4b3a9b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HeartbeatLevelOutput.java @@ -0,0 +1,22 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-08-15 17:04 + **/ +@Data +public class HeartbeatLevelOutput { + + @ApiModelProperty("当前用户与AI的心动关系") + private AiUserHeartbeatRelationOutput aiUserHeartbeatRelation; + + @ApiModelProperty("心动等级字典列表") + private List heartbeatLeveLDictList; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HeartbeatRelationListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HeartbeatRelationListOutput.java new file mode 100644 index 0000000..b5a2fec --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HeartbeatRelationListOutput.java @@ -0,0 +1,62 @@ +package com.sonic.frog.domain.output; + +import com.sonic.frog.enums.HeartbeatLevelEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-08-25 11:43 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class HeartbeatRelationListOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("ai所属用户id") + private Long userId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("出生日期") + private LocalDateTime birthday; + + @ApiModelProperty("角色名称") + private String roleName; + + @ApiModelProperty("性格名称") + private String characterName; + + @ApiModelProperty("标签名称") + private String tagName; + + @ApiModelProperty("心动等级") + private HeartbeatLevelEnum heartbeatLevel; + + @ApiModelProperty("心动值") + private BigDecimal heartbeatVal; + + @ApiModelProperty("心动等级数字") + private Integer heartbeatLevelNum; + + @ApiModelProperty("关系显示开关 默认关闭 0:关闭 1:打开") + private Boolean isShow; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HomeRecommendListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HomeRecommendListOutput.java new file mode 100644 index 0000000..3222fcd --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HomeRecommendListOutput.java @@ -0,0 +1,25 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class HomeRecommendListOutput { + + @ApiModelProperty("第多少张图片时能够被匹配上") + private Integer num; + + @ApiModelProperty("列表") + private List list; + + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HomeRecommendOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HomeRecommendOutput.java new file mode 100644 index 0000000..a0e8212 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HomeRecommendOutput.java @@ -0,0 +1,59 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class HomeRecommendOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("0,男;1,女;2,自定义") + @NotNull + private Integer sex; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("年龄") + private Integer age; + + @ApiModelProperty("点赞数") + private Integer likedCount; + + @ApiModelProperty("角色") + private String role; + + @ApiModelProperty("性格") + private String character; + + @ApiModelProperty("标签") + private String tag; + + @ApiModelProperty("简介") + private String introduction; + + @ApiModelProperty("形象图") + private String imageUrl; + + @ApiModelProperty("心动值") + private BigDecimal heartbeatVal; + + @ApiModelProperty("相册列表") + private List albumList; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HomeRecommendV2Output.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HomeRecommendV2Output.java new file mode 100644 index 0000000..bda79cf --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/HomeRecommendV2Output.java @@ -0,0 +1,22 @@ +package com.sonic.frog.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class HomeRecommendV2Output { + + private List mostChat; + + private List mustCrush; + + private List starAChat; + + private List mustGifted; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ListAiAlbumOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ListAiAlbumOutput.java new file mode 100644 index 0000000..bbec75d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ListAiAlbumOutput.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 - 2050 zyp.All Rights Reserved. + * + */ +package com.sonic.frog.domain.output; + +import com.sonic.frog.domain.entity.Liked; +import com.sonic.frog.enums.LockStatusEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import java.io.Serializable; + +/** + * Created by xi.he on 2020/2/5 + */ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ListAiAlbumOutput implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty("图片id") + private Long albumId; + + @ApiModelProperty("ai的id") + private Long aiId; + + @ApiModelProperty("图片地址") + private String imgUrl; + + @ApiModelProperty("解锁价格") + private Long unlockPrice; + + @ApiModelProperty("图片宽") + private String width; + + @ApiModelProperty("图片高") + private String height; + + @ApiModelProperty("点赞数") + private Integer likedCount; + + @ApiModelProperty("当前登录用户对相册的点赞状态 已点赞、取消点赞") + private Liked.LikedStatus likedStatus; + + @ApiModelProperty("是否默认相册图片") + private Boolean isDefault; + + @ApiModelProperty("图片是否解锁") + private LockStatusEnum lockStatus; + + @ApiModelProperty("模糊图片1") + private String img1; + + @ApiModelProperty("模糊图片2") + private String img2; + + @ApiModelProperty("模糊图片3") + private String img3; + + @ApiModelProperty + private Integer imgOrder; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/MeetSdOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/MeetSdOutput.java new file mode 100644 index 0000000..5163593 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/MeetSdOutput.java @@ -0,0 +1,19 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MeetSdOutput { + + @ApiModelProperty("是否能够调用绑定") + private Boolean bd; + + @ApiModelProperty("是否能够调用爱慕者推荐") + private Boolean rc; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/SignInListOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/SignInListOutput.java new file mode 100644 index 0000000..4c23ca0 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/SignInListOutput.java @@ -0,0 +1,30 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author zzhan + * @Description 七日签到数据列表 + * @Date 2024/6/17 11:21 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SignInListOutput { + + @ApiModelProperty(value = "PST 天(yyyy-MM-dd)") + private String dayStr; + + @ApiModelProperty(value = "是否已签到") + private Boolean signIn; + + @ApiModelProperty(value = "得到coin的数量") + private Integer coinNum; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/SignInRoundOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/SignInRoundOutput.java new file mode 100644 index 0000000..c4d99ae --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/SignInRoundOutput.java @@ -0,0 +1,29 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @Author zzhan + * @Description 七日签到数据列表 + * @Date 2024/6/17 11:21 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SignInRoundOutput { + + @ApiModelProperty(value = "最大连续签到天数") + private Integer continuousDays; + + @ApiModelProperty(value = "签到周期基础数据") + private List list; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/StartChatOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/StartChatOutput.java new file mode 100644 index 0000000..fb60fb7 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/StartChatOutput.java @@ -0,0 +1,39 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StartChatOutput { + + @ApiModelProperty("AI的id") + private Long aiId; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("头像") + private String headImg; + + @ApiModelProperty("被喜欢数") + private Integer likedNum; + + @ApiModelProperty("开场白语音地址") + private String dialoguePrologueSound; + + @ApiModelProperty("主动聊天内容") + private List supportingContentList; + + @ApiModelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ThirdLoginOrRegisterOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ThirdLoginOrRegisterOutput.java new file mode 100644 index 0000000..1b41044 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ThirdLoginOrRegisterOutput.java @@ -0,0 +1,21 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 登录或注册时的账号验证结果 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ThirdLoginOrRegisterOutput { + + @ApiModelProperty("登录token") + private String token; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/UserBaseInfoOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/UserBaseInfoOutput.java new file mode 100644 index 0000000..e1c14f3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/UserBaseInfoOutput.java @@ -0,0 +1,48 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-09-16 20:31 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserBaseInfoOutput { + + @ApiModelProperty("用户ID") + private Long userId; + @ApiModelProperty("headImage") + private String headImage; + @ApiModelProperty("昵称") + private String nickname; + @ApiModelProperty("性别") + private Integer sex; + @ApiModelProperty("ID编号") + private String idCard; + @ApiModelProperty("生日") + private LocalDateTime birthday; + @ApiModelProperty("账号类型") + private String thirdType; + @ApiModelProperty("账号昵称") + private String thirdNickname; + @ApiModelProperty("账号邮箱") + private String thirdEmail; + @ApiModelProperty("是否需要强制完善用户基础信息") + private Boolean cpUserInfo; + @ApiModelProperty("是否是会员") + private Boolean isMember; + @ApiModelProperty("已创建AI数量") + private Integer createdAiCount; + @ApiModelProperty("可创建AI数量") + private Integer canCreateAiCount; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/UserCreateCountOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/UserCreateCountOutput.java new file mode 100644 index 0000000..57aa4df --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/UserCreateCountOutput.java @@ -0,0 +1,39 @@ +package com.sonic.frog.domain.output; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-09-15 10:31 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserCreateCountOutput { + + @ApiModelProperty(value = "免费创作次数") + private Integer freeNum; + + @ApiModelProperty(value = "会员赠送创作次数") + private Integer memberNum; + + @ApiModelProperty(value = "购买创作次数") + private Integer buyNum; + + @ApiModelProperty(value = "已使用免费创作次数") + private Integer usedFreeNum; + + @ApiModelProperty(value = "已使用会员赠送创作次数") + private Integer usedMemberNum; + + @ApiModelProperty(value = "已使用购买创作次数") + private Integer usedBuyNum; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ViewUnlockAlbumImgOutput.java b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ViewUnlockAlbumImgOutput.java new file mode 100644 index 0000000..cd41887 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/domain/output/ViewUnlockAlbumImgOutput.java @@ -0,0 +1,28 @@ +package com.sonic.frog.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2025-08-22 11:40 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ViewUnlockAlbumImgOutput { + + @ApiModelProperty("模糊图片1") + private String img1; + + @ApiModelProperty("模糊图片2") + private String img2; + + @ApiModelProperty("模糊图片3") + private String img3; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/AdvertiseBizType.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/AdvertiseBizType.java new file mode 100644 index 0000000..884025a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/AdvertiseBizType.java @@ -0,0 +1,9 @@ +package com.sonic.frog.enums; + +import lombok.Getter; + +@Getter +public enum AdvertiseBizType { + //ai轮播推荐 + AI_CAROUSEL_RECOMMEND +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/AgeTypeEnum.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/AgeTypeEnum.java new file mode 100644 index 0000000..75b2bbe --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/AgeTypeEnum.java @@ -0,0 +1,24 @@ +package com.sonic.frog.enums; + +import lombok.Getter; + +@Getter +public enum AgeTypeEnum { + + AGE_1(18, 24), + AGE_2(25, 34), + AGE_3(35, 44), + AGE_4(45, 54), + AGE_5(55, 100), + ; + + + private int startAge; + + private int endAge; + + AgeTypeEnum(int startAge, int endAge) { + this.startAge = startAge; + this.endAge = endAge; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/AiDictTypeEnum.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/AiDictTypeEnum.java new file mode 100644 index 0000000..33c9740 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/AiDictTypeEnum.java @@ -0,0 +1,18 @@ +package com.sonic.frog.enums; + +import lombok.Getter; + +/** + * @description: Ai字典类型枚举 + * @author: mzc + * @date: 2025-07-11 13:52 + **/ +@Getter +public enum AiDictTypeEnum { + //角色 + ROLE, + //性格 + CHARACTER, + //标签 + TAG +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/BizResultCode.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/BizResultCode.java new file mode 100644 index 0000000..b4e63b8 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/BizResultCode.java @@ -0,0 +1,103 @@ +package com.sonic.frog.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum BizResultCode implements ApiResultCode { + + /** + * 可以在此处扩展服务自身需要用到的错误码信息 + */ + SYS_PARAMETERS_VALIDATE_EXCEPTION("10010003", "Parameter validation error"), + SYS_VALIDATION_FAILED_ERROR("1001011", "Validation failed, invalid request"), + + ACCOUNT_NOT_REGISTER("-1", "Incorrect username or password"), + FACEBOOK_ACCOUNT_ERROR("-1", "Facebook account could not be verified"), + FACEBOOK_INVALID("-1", "Facebook login is invalid, please login again"), + + APPLE_NETWORK_ERROR("10000001", "apple network error"), + + MISS_PARAM_ERROR("1001010", "Missing parameter"), + + DISCORD_NETWORK_ERROR("10000001", "discord network error"), + GOOGLE_ID_GET_ERROR("10010151", "GOOGLE ID GET ERROR"), + + AUTH_FAIL("10010151", "Authorization failed"), + DEVICE_BLOCK_ERROR("10010152", "This device has been banned"), + FREEZE_ERROR("10010153", "Account has been frozen"), + + ; + + + private final String errorCode; + private final String errorMsg; + + BizResultCode(String errorCode, String errorMsg) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1002"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + + /** + * 校验方法 + * + * @param expect + * @param code + */ + public void check(boolean expect, String code) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(code, this.name().equals(message) ? this.getErrorMsg() : message); + } + } + + /** + * 校验方法 + * + * @param expect + * @param code + * @param message + */ + public void check(boolean expect, String code, String message) { + if (expect) { + throw new BizException(code, message); + } + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/BubbleUnlockTypeEnum.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/BubbleUnlockTypeEnum.java new file mode 100644 index 0000000..34da17b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/BubbleUnlockTypeEnum.java @@ -0,0 +1,14 @@ +package com.sonic.frog.enums; + +import lombok.Getter; + +/** + * 聊天气泡解锁类型枚举 + */ +@Getter +public enum BubbleUnlockTypeEnum { + //会员 + MEMBER, + //心动等级 + HEARTBEAT_LEVEL +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/BusinessException.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/BusinessException.java new file mode 100644 index 0000000..09c050a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/BusinessException.java @@ -0,0 +1,39 @@ +package com.sonic.frog.enums; + + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.GlobalResultCode; + +public class BusinessException extends BizException { + + private static final long serialVersionUID = -5317007026578376164L; + + /** + * 错误码 + */ + private String errorCode; + /** + * 错误描述 + */ + private String errorMsg; + + /** + * @param errorCode + * @param errorMsg + */ + public BusinessException(String errorCode, String errorMsg) { + super(GlobalResultCode.INVALID_PARAMS.getErrorCode(), String.format("BusinessException{errorCode:%s, errorMsg:%s}", errorCode, errorMsg)); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + @Override + public String getErrorCode() { + return errorCode; + } + + @Override + public String getErrorMsg() { + return errorMsg; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/Constants.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/Constants.java new file mode 100644 index 0000000..af68728 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/Constants.java @@ -0,0 +1,89 @@ +package com.sonic.frog.enums; + +import com.google.common.collect.Lists; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 常量 + */ +public class Constants { + + /** + * google 验证返回字段 + */ + public static final String AUD = "aud"; + + /** + * 单个用户使用设备号的最大数量 + */ + public static final Integer USER_DEVICE_MAX_COUNT = 5; + + /** + * 单个 + */ + public static final Integer DEVICE_USER_MAX_COUNT = 5; + + /** + * 每日最大的登录次数 + */ + public static final Integer PASSWORD_LOGIN_MAX_NUM = 20; + + /** + * 每日最大的登录次数 + */ + public static final Integer PASSWORD_LOGIN_MIN_NUM = 4; + + /** + * 心动值单价 + */ + public static final Long HEARTBEAT_VAL_PRICE = 500L; + + /** + * 基础分值 + */ + public static final BigDecimal threshold = new BigDecimal("15"); + + /** + * 打赏,解锁图片,平台实际所得比例 + */ + public static final BigDecimal PLATFORM_RATE = new BigDecimal("1"); + + + /** + * 打赏,解锁图片,用户实际所得比例 + */ + public static final BigDecimal USER_RATE = new BigDecimal("0"); + + /** + * 会员可创建AI数量 + */ + public static final Integer TOTAL_CREATE_AI_NUM = 5; + + /** + * 普通用户可创建AI数量 + */ + public static final Integer DEFAULT_CREATE_AI_NUM = 1; + + + /** + * 创建ai没有限制的用户id列表 + */ + public static final List CREATE_AI_NO_LIMIT_USER_ID_LIST = Lists.newArrayList(446425109102593L, 446339794862081L, 446339874553857L, 446339809542145L, 439058245812225L); + + /** + * 创建ai没有限制的创建数量 + */ + public static final Integer CREATE_AI_NO_LIMIT_NUM = 100; + + /** + * 默认背景 + */ + public static final String DEFAULT_BACKGROUND = "DEFAULT"; + + /** + * 一个Coin币对应的心动值 + */ + public static final BigDecimal ONE_COIN_HEARTBEAT_VAL= new BigDecimal("0.2"); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/DeductionTypeEnum.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/DeductionTypeEnum.java new file mode 100644 index 0000000..3448109 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/DeductionTypeEnum.java @@ -0,0 +1,48 @@ +package com.sonic.frog.enums; + +import com.sonic.lion.lib.enums.BizType; +import lombok.Getter; + +@Getter +public enum DeductionTypeEnum { + //文本 + TEXT(1, 100L, BizType.TEXT_MODEL), + //发送或听取语音 + VOICE(2, 1000L, BizType.SEND_VOICE), + //语音通话 + VOICE_CALL(3, 2000L, BizType.VOICE_CALL), + + ; + private final Integer index; + + /** + * 每一次预扣除金额 单位分 + */ + private final Long amount; + + /** + * 对应的付款类型 + */ + private final BizType bizType; + + DeductionTypeEnum(Integer index, Long amount, BizType bizType) { + this.index = index; + this.amount = amount; + this.bizType = bizType; + } + + /** + * 通过索引获取枚举 + * + * @param index + * @return + */ + public static DeductionTypeEnum getDeductionTypeEnum(Integer index) { + for (DeductionTypeEnum value : DeductionTypeEnum.values()) { + if (value.index.equals(index)) { + return value; + } + } + return null; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/HeartbeatLevelEnum.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/HeartbeatLevelEnum.java new file mode 100644 index 0000000..ec1676f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/HeartbeatLevelEnum.java @@ -0,0 +1,85 @@ +package com.sonic.frog.enums; + +import lombok.Getter; + +import java.math.BigDecimal; + +@Getter +public enum HeartbeatLevelEnum { + LEVEL_1(1, new BigDecimal("0.1"), "ACQUAINTANCE", "Initial acquaintance","Lv.1"), + LEVEL_2(2, new BigDecimal("0.3"), "ACQUAINTANCE", "Initial acquaintance","Lv.2"), + LEVEL_3(3, new BigDecimal("0.5"), "FRIEND", "Friend","Lv.3"), + LEVEL_4(4, new BigDecimal("1"), "FRIEND", "Friend","Lv.4"), + LEVEL_5(5, new BigDecimal("2"), "FLIRT", "Flirt","Lv.5"), + LEVEL_6(6, new BigDecimal("3"), "FLIRT", "Flirt","Lv.6"), + LEVEL_7(7, new BigDecimal("4"), "LOVE", "Love","Lv.7"), + LEVEL_8(8, new BigDecimal("5"), "LOVE", "Love","Lv.8"), + LEVEL_9(9, new BigDecimal("6"), "MARRIAGE", "Marriage","Lv.9"), + LEVEL_10(10, new BigDecimal("7"), "MARRIAGE", "Marriage","Lv.10"), + ; + + /** + * 对应等级数字num + */ + private final int num; + /** + * 对应等级24小时未聊天需要扣除的心动值 + */ + private BigDecimal subtractHeartbeatVal; + + /** + * 关系阶段代码 + */ + private String relationStage; + /** + * 关系阶段名称 + */ + private String relationStageName; + + /** + * 对应的等级名称 + */ + private String levelName; + + HeartbeatLevelEnum(int num, BigDecimal subtractHeartbeatVal, String relationStage, String relationStageName, String levelName) { + this.num = num; + this.subtractHeartbeatVal = subtractHeartbeatVal; + this.relationStage = relationStage; + this.relationStageName = relationStageName; + this.levelName = levelName; + } + + /** + * 判断是否升级 + * + * @param oldLevel + * @param newLevel + * @return + */ + public static Boolean isUpgrade(HeartbeatLevelEnum oldLevel, HeartbeatLevelEnum newLevel) { + if (oldLevel == null && newLevel != null) { + return true; + } else if (oldLevel != null && newLevel != null && newLevel.num > oldLevel.num) { + return true; + } + return false; + } + + + /** + * 判断是否降级 + * + * @param oldLevel + * @param newLevel + * @return + */ + public static Boolean isDowngrade(HeartbeatLevelEnum oldLevel, HeartbeatLevelEnum newLevel) { + if (oldLevel != null && newLevel == null) { + return true; + } else if (oldLevel != null && newLevel != null && newLevel.num < oldLevel.num) { + return true; + } + return false; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/LockStatusEnum.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/LockStatusEnum.java new file mode 100644 index 0000000..7b5ae23 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/LockStatusEnum.java @@ -0,0 +1,14 @@ +package com.sonic.frog.enums; + +import lombok.Getter; + +/** + * 相册锁状态 + */ +@Getter +public enum LockStatusEnum { + //上锁 + LOCK, + //解锁 + UNLOCK; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/MessageTypeEnum.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/MessageTypeEnum.java new file mode 100644 index 0000000..642bb70 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/MessageTypeEnum.java @@ -0,0 +1,28 @@ +package com.sonic.frog.enums; + +import lombok.Getter; + +@Getter +public enum MessageTypeEnum { + + TEXT(0, "文本消息"), + IMAGE(1, "图片消息"), + VOICE(2, "语音消息"), + VIDEO(3, "视频消息"), + GEO(4, "地理位置消息"), + FILE(6, "文件消息"), + NOTIFY(10, "提示消息"), + CUSTOM(100, "自定义消息"), + + ; + + + private int code; + + private String desc; + + MessageTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/PayGenrtatorCodeType.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/PayGenrtatorCodeType.java new file mode 100644 index 0000000..e30bef5 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/PayGenrtatorCodeType.java @@ -0,0 +1,50 @@ +package com.sonic.frog.enums; + +/** + * 交易业务分类:根据业务分类 + * @author Xi.He + */ +public enum PayGenrtatorCodeType { + TRADE(1, "交易号"), + WITHDRAW(2, "提现号"), + CHANNEL_BILL(3,"支付流水号"), + ACCOUNT_BILL(4,"用户流水号"), + REFUND_BILL(5,"退款流水号"), + ; + /** 编码 */ + private int value; + /** 名称 */ + private String desc; + + PayGenrtatorCodeType(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static PayGenrtatorCodeType get(int value) { + for (PayGenrtatorCodeType payGenrtatorCodeType : values()) { + if (payGenrtatorCodeType.getValue() == value) { + return payGenrtatorCodeType; + } + } + return null; + } + + /** + * 根据站点类型获取交易枚举类型 + * @param siteType + * @return + */ + public static PayGenrtatorCodeType getTradeType(String siteType) { + return TRADE; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/SendGiftSceneEnum.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/SendGiftSceneEnum.java new file mode 100644 index 0000000..9625b7e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/SendGiftSceneEnum.java @@ -0,0 +1,16 @@ +package com.sonic.frog.enums; + +import lombok.Getter; + +/** + * @description: + * @author: mzc + * @date: 2025-09-22 15:35 + **/ +@Getter +public enum SendGiftSceneEnum { + //IM + IM, + //首页 + HOME, +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/SignInCoinNumEnum.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/SignInCoinNumEnum.java new file mode 100644 index 0000000..8e34e89 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/SignInCoinNumEnum.java @@ -0,0 +1,41 @@ +package com.sonic.frog.enums; + +import lombok.Getter; + +/** + * 签到金币枚举 + */ +@Getter +public enum SignInCoinNumEnum { + + DAY_1(1, 500), + DAY_2(2, 500), + DAY_3(3, 500), + DAY_4(4, 500), + DAY_5(5, 500), + DAY_6(6, 500), + DAY_7(7, 500); + + /** + * 第几天 + */ + private final Integer dayNum; + /** + * 奖励金币的数量 + */ + private final Integer coinNum; + + SignInCoinNumEnum(Integer dayNum, Integer coinNum) { + this.dayNum = dayNum; + this.coinNum = coinNum; + } + + public static Integer getCoinNum(Integer dayNum) { + for (SignInCoinNumEnum value : SignInCoinNumEnum.values()) { + if (value.dayNum.equals(dayNum)) { + return value.coinNum; + } + } + return 0; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/enums/ToastResultCode.java b/sonic-frog/server/src/main/java/com/sonic/frog/enums/ToastResultCode.java new file mode 100644 index 0000000..2a5a2bb --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/enums/ToastResultCode.java @@ -0,0 +1,128 @@ +package com.sonic.frog.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum ToastResultCode implements ApiResultCode { + SYS_SYSTEM_EXCEPTION("", "System error"), + + SYS_PARAMETERS_VALIDATE_EXCEPTION("-1", "sys.parameters.validate.exception"), + + NO_LOGIN("0001", "User is not logged in"), + USER_NOT_EXIST("0002", "User does not exist"), + APP_CLIENT_NOT_EXIST("0003", "Login client does not exist"), + APP_CLIENT_NOT_ALLOW("0004", "Not authorized to log in to the client"), + SESSION_EXPIRED("0005", "Session is expired"), + SESSION_INVALID("0006", "Session is invalid"), + PASSWORD_INVALID("0007", "Invalid account or password"), + /** + * 第三方登陆授权失败 + */ + AUTH_FAIL("0008", "auth.fail"), + USER_FROZEN("0009", "User is frozen"), + ACCOUNT_DOES_NOT_EXIST("The account does not exist"), + + AGE_CHECK("0010", "User age cannot be less than 18 years old"), + + USER_NICKNAME_EXIST("0010", "User nickname already exists"), + + SYS_PERMISSION_DENIED("0011", "Insufficient permissions"), + + AI_USER_NOT_EXIST("0012", "AI user does not exist"), + + ALBUM_IS_DELETED("0012", "Image has been deleted"), + ALBUM_NOT_EXIST("0013", "Image does not exist"), + ALBUM_DEFAULT_CAN_NOT_DELETE("0014", "Default image cannot be deleted"), + ALBUM_ENCRYPT_CAN_NOT_LIKE("", "Encrypted image cannot be liked"), + ALBUM_DEFAULT_CAN_NOT_MODIFY_PAY("", "Default image cannot be changed to paid"), + ALBUM_ADD_MAX_ONE("", "Can add up to one image"), + + GEN_IMAGE_LIMIT_ERROR("", "Maximum of 10 image changes within 24 hours, please try again in %s hours and %s minutes"), + + CHAT_BUBBLE_NOT_EXIST("0013", "Chat bubble does not exist"), + BACKGROUND_IS_DELETED("0014", "Background has been deleted"), + BACKGROUND_NOT_EXIST("0015", "Background does not exist"), + BACKGROUND_ADD_MAX_ONE("", "Can add up to one background"), + + GIFT_NOT_EXIST("0016", "Gift does not exist"), + NOT_MEMBER_CREATE_AI_ONE("017", "Non-members can only create 1 AI"), + MEMBER_CREATE_AI_MAX_NUM("018", "Members can create up to 5 AIs"), + + USER_CREATE_COUNT_NONE("019", "No creation attempts remaining"), + + PARAM_NOT_NULL("0020", "Parameter cannot be empty"), + PARAM_LEN_MIN_ERROR("0021", "Length is insufficient"), + + + ; + + + private final String errorCode; + private final String errorMsg; + + ToastResultCode(String errorMsg) { + this.errorCode = ""; + this.errorMsg = errorMsg; + } + + ToastResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect, String msg) { + if (expect) { + if (StringUtils.isNotBlank(msg)) { + throw new BizException(this.getErrorCode(), msg); + + } else { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/EventType.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/EventType.java new file mode 100644 index 0000000..58b8338 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/EventType.java @@ -0,0 +1,46 @@ +package com.sonic.frog.event.inner; + +import com.sonic.frog.config.EventConfig; +import com.sonic.common.event.Event; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义当前系统的消息 + * @author code + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** 事件定义 */ + DEMO_CREATED(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "demo_created", "demo 创建"), + + AI_IM_INFO(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "ai_im_info", "AI的IM基础信息"), + + CALC_HEARTBEAT_LEVEL(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "calc_heartbeat_level", "心动等级计算"), + + CALC_HEARTBEAT_SCORE(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "calc_heartbeat_score", "心动分计算"), + + SUBTRACT_HEARTBEAT_VAL(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "subtract_heartbeat_val", "扣减心动值"), + + AI_USER_STAT(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "ai_user_stat", "ai用户数据统计"), + + CALC_HEARTBEAT_RANK(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "calc_heartbeat_rank", "心动榜单总分值计算"), + + AI_CHANGE(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "ai_change", "ai创建,编辑,删除变化"), + + + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/AIChangeHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/AIChangeHandler.java new file mode 100644 index 0000000..7b82c01 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/AIChangeHandler.java @@ -0,0 +1,140 @@ +package com.sonic.frog.event.inner.handler; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Lists; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.cow.lib.client.ContentClient; +import com.sonic.cow.lib.client.VoiceClient; +import com.sonic.cow.lib.input.VoiceTtsInput; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.AiUserExt; +import com.sonic.frog.domain.entity.TimbreDict; +import com.sonic.frog.domain.input.ClassificationListInput; +import com.sonic.frog.event.inner.EventType; +import com.sonic.frog.event.inner.payload.AiChangePayload; +import com.sonic.frog.service.AiUserExtService; +import com.sonic.frog.service.AiUserService; +import com.sonic.frog.service.HomeClassificationService; +import com.sonic.frog.service.TimbreDictService; +import com.sonic.shark.lib.client.S3Client; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Base64; +import java.util.List; + +/** + * @author code + */ +@Slf4j +@Component +public class AIChangeHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private AiUserService aiUserService; + @Autowired + private HomeClassificationService homeClassificationService; + @Autowired + private AiUserExtService aiUserExtService; + @Autowired + private VoiceClient voiceClient; + @Autowired + private ContentClient contentClient; + @Autowired + private S3Client s3Client; + @Autowired + private TimbreDictService timbreDictService; + + + @Override + public void onEvent(Event event) { + AiChangePayload payload = event.normalizedData(AiChangePayload.class); + log.info("AIChangeHandler payload:{}", payload); + if (payload.getAiId() == null) { + return; + } + try { + Long aiId = payload.getAiId(); + //获取ai的基础信息 + AiUser aiUser = aiUserService.getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, aiId)); + //更新首页缓存 + updateHomeCache(aiUser); + //开场白更新需要生成开场白语音 + if (payload.getIsDialoguePrologueChange() != null && payload.getIsDialoguePrologueChange()) { + generateDialoguePrologue(aiId); + } + //创建AI生成三条辅助聊天内容 + if (payload.getIsCreate() != null && payload.getIsCreate()) { + generateSupportingContent(aiUser.getUserId(), aiId); + } + } catch (Exception e) { + log.error("AIChangeHandler error:", e); + } + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.AI_CHANGE.getEventCode(), this); + } + + /** + * 更新首页缓存 + * + * @param aiUser + */ + private void updateHomeCache(AiUser aiUser) { + //更新首页对应性格缓存 + String characterCode = aiUser.getCharacterCode(); + ClassificationListInput input = ClassificationListInput.builder() + .characterCodeList(Lists.newArrayList(characterCode)) + .build(); + homeClassificationService.homeClassificationUpdateCache(input); + + //更新首页All缓存 + ClassificationListInput allInput = ClassificationListInput.builder() + .build(); + homeClassificationService.homeClassificationUpdateCache(allInput); + } + + /** + * 生成开场白语音并上传到S3,保存到表里 + * + * @param aiId + */ + private void generateDialoguePrologue(Long aiId) { + AiUserExt aiUserExt = aiUserExtService.getAiUserExtByAiId(aiId); + //开场白 + String dialoguePrologue = aiUserExt.getDialoguePrologue(); + + //语音类型,音高,语速 + TimbreDict timbreDict = timbreDictService.getTimbreDictByCode(aiUserExt.getDialogueTimbreCode()); + + //调用Cow服务内部api生成语音 base64 + VoiceTtsInput voiceTtsInput = new VoiceTtsInput(); + voiceTtsInput.setUserId(-1L); + voiceTtsInput.setAiId(aiId); + voiceTtsInput.setText(dialoguePrologue); + voiceTtsInput.setVoiceType(timbreDict != null ? timbreDict.getVoiceType() : null); + voiceTtsInput.setSpeechRate(Integer.valueOf(aiUserExt.getDialogueSpeechRate())); + voiceTtsInput.setPitchRate(Integer.valueOf(aiUserExt.getDialoguePitch())); + String ttsBase64 = voiceClient.tts(voiceTtsInput); + //调用Shark服务把语音上传到S3 + byte[] bytes = Base64.getDecoder().decode(ttsBase64); + String mp3Url = s3Client.uploadAwsS3(bytes, "SOUND_PATH", "mp3"); + //保存生成的语音文件到数据库 + aiUserExtService.updateAiUserDialoguePrologueSound(mp3Url, aiId); + } + + private void generateSupportingContent(Long userId, Long aiId) { + //调用Cow服务内部api生成三条辅助聊天内容 + List supContentList = contentClient.genSupContent(userId, aiId); + //保存到数据库,JSON格式 + aiUserExtService.updateAiUserSupportingContent(supContentList, aiId); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/AIImInfoHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/AIImInfoHandler.java new file mode 100644 index 0000000..86590c3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/AIImInfoHandler.java @@ -0,0 +1,63 @@ +package com.sonic.frog.event.inner.handler; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.event.inner.EventType; +import com.sonic.frog.event.inner.payload.AiImInfoPayload; +import com.sonic.frog.service.AiUserService; +import com.sonic.pigeon.lib.client.ImUserClient; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import com.sonic.pigeon.lib.input.CreateImUserInput; +import com.sonic.pigeon.lib.input.EditImUserInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.sql.Wrapper; + +/** + * @author code + */ +@Slf4j +@Component +public class AIImInfoHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private AiUserService aiUserService; + @Autowired + private ImUserClient imUserClient; + + @Override + public void onEvent(Event event) { + AiImInfoPayload payload = event.normalizedData(AiImInfoPayload.class); + //获取IM的基础信息 + AiUser aiUser = aiUserService.getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, payload.getAiId())); + //根据操作类型判断是创建还是修改 + if(AiImInfoPayload.OptType.ADD == payload.getOptType()){ + CreateImUserInput input = new CreateImUserInput(); + input.setUserId(aiUser.getAiId()); + input.setNickname(aiUser.getNickname()); + input.setHeadImage(aiUser.getHeadImg()); + input.setImUserType(ImUserTypeEnum.r); + imUserClient.createImUser(input); + }else if(AiImInfoPayload.OptType.UPDATE == payload.getOptType()){ + EditImUserInput input = new EditImUserInput(); + input.setImUserType(ImUserTypeEnum.r); + input.setUserId(aiUser.getAiId()); + input.setNickname(aiUser.getNickname()); + input.setHeadImage(aiUser.getHeadImg()); + imUserClient.editImUser(input); + } + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.AI_IM_INFO.getEventCode(), this); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/AiUserStatHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/AiUserStatHandler.java new file mode 100644 index 0000000..ed514eb --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/AiUserStatHandler.java @@ -0,0 +1,43 @@ +package com.sonic.frog.event.inner.handler; + +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.event.inner.EventType; +import com.sonic.frog.event.inner.payload.AiUserStatPayload; +import com.sonic.frog.event.inner.payload.CalcHeartbeatLevelPayload; +import com.sonic.frog.service.AiUserHeartbeatRelationService; +import com.sonic.frog.service.AiUserStatService; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * AI用户数据比诸 + * + * @author mzc + */ +@Slf4j +@Component +public class AiUserStatHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private AiUserStatService aiUserStatService; + + @Override + public void onEvent(Event event) { + AiUserStatPayload payload = event.normalizedData(AiUserStatPayload.class); + log.info("AiUserStatHandler payload:{}", payload); + aiUserStatService.aiUserStat(payload); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.AI_USER_STAT.getEventCode(), this); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/CalcAiUserHeartbeatRankHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/CalcAiUserHeartbeatRankHandler.java new file mode 100644 index 0000000..a9a7061 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/CalcAiUserHeartbeatRankHandler.java @@ -0,0 +1,87 @@ +package com.sonic.frog.event.inner.handler; + +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.dao.AiUserStatDao; +import com.sonic.frog.event.inner.EventType; +import com.sonic.frog.event.inner.payload.CalcAiUserHeartbeatRankPayload; +import com.sonic.frog.service.AiUserHeartbeatRankService; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; + +import static com.sonic.frog.enums.Constants.threshold; + +/** + * 计算心动值榜单 + * @author code + */ +@Slf4j +@Component +public class CalcAiUserHeartbeatRankHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private AiUserHeartbeatRankService aiUserHeartbeatRankService; + @Autowired + private AiUserStatDao aiUserStatDao; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @Override + public void onEvent(Event event) { + CalcAiUserHeartbeatRankPayload payload = event.normalizedData(CalcAiUserHeartbeatRankPayload.class); + log.info("CalcAiUserHeartbeatRankHandler payload:{}", payload); + //加锁处理 + RedisLock redisLock = new RedisLock(redisKeyUtils.calcHeartbeatRankLockKey(payload.getUserId()), redisWrapper); + redisLock.tryAcquireRun(() -> { + handler(payload); + return true; + }); + } + + /** + * 处理 + * @param payload + */ + private void handler(CalcAiUserHeartbeatRankPayload payload) { + //计算 当前AI与所有和他聊天过的用户的心动值总和的榜单 + aiUserStatDao.incrementHeartbeatVal(payload.getAiId(), payload.getNewHeartbeatVal().subtract(payload.getOldHeartbeatVal())); + + BigDecimal orgVal = payload.getOldHeartbeatVal(); + BigDecimal newVal = payload.getNewHeartbeatVal(); + //比较的基础值 + BigDecimal heartbeatVal = BigDecimal.ZERO; + + log.info("CalcAiUserHeartbeatRankHandler orgVal:{}, newVal:{}, threshold:{}", orgVal, newVal, threshold); + // 场景1:用户从15分以下升到15分以上,此时触发 + 新总分值 + if (orgVal.compareTo(threshold) < 0 && newVal.compareTo(threshold) >= 0) { + heartbeatVal = new BigDecimal(newVal.intValue()); + } + // 场景2:用户之前是15分,现在也是15分,此时触发 ± (新总分值 - 老总分值) + else if (orgVal.compareTo(threshold) > 0 && newVal.compareTo(threshold) > 0) { + heartbeatVal = newVal.subtract(orgVal); + } + // 场景3:用户从15分以上降到15分以下,此时触发 - 老分值 + else if (orgVal.compareTo(threshold) > 0 && newVal.compareTo(threshold) < 0) { + heartbeatVal = orgVal.negate(); + } + log.info("CalcAiUserHeartbeatRankHandler heartbeatVal:{}", heartbeatVal); + //计算 当前用户和所有聊天过的AI的心动值总和榜单 + aiUserHeartbeatRankService.incrementHeartbeatVal(payload.getUserId(), heartbeatVal); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.CALC_HEARTBEAT_RANK.getEventCode(), this); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/CalcHeartbeatLevelHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/CalcHeartbeatLevelHandler.java new file mode 100644 index 0000000..f180540 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/CalcHeartbeatLevelHandler.java @@ -0,0 +1,50 @@ +package com.sonic.frog.event.inner.handler; + +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.event.inner.EventType; +import com.sonic.frog.event.inner.payload.CalcHeartbeatLevelPayload; +import com.sonic.frog.service.AiUserHeartbeatRelationService; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author code + */ +@Slf4j +@Component +public class CalcHeartbeatLevelHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @Override + public void onEvent(Event event) { + CalcHeartbeatLevelPayload payload = event.normalizedData(CalcHeartbeatLevelPayload.class); + log.info("CalcHeartbeatLevelHandler payload:{}", payload); + //redis键 + String calcHeartbeatLevelLockKey = redisKeyUtils.calcHeartbeatLevelLockKey(payload.getUserId(), payload.getAiId()); + //加锁处理 + RedisLock redisLock = new RedisLock(calcHeartbeatLevelLockKey, redisWrapper); + redisLock.tryAcquireRun(() -> { + aiUserHeartbeatRelationService.calcAiUserHeartbeatLevel(payload); + return true; + }); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.CALC_HEARTBEAT_LEVEL.getEventCode(), this); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/CalcHeartbeatScoreHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/CalcHeartbeatScoreHandler.java new file mode 100644 index 0000000..925801e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/CalcHeartbeatScoreHandler.java @@ -0,0 +1,40 @@ +package com.sonic.frog.event.inner.handler; + +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.event.inner.EventType; +import com.sonic.frog.event.inner.payload.CalcHeartbeatLevelPayload; +import com.sonic.frog.event.inner.payload.CalcHeartbeatScorePayload; +import com.sonic.frog.service.AiUserHeartbeatRelationService; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author code + */ +@Slf4j +@Component +public class CalcHeartbeatScoreHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + + @Override + public void onEvent(Event event) { + CalcHeartbeatScorePayload payload = event.normalizedData(CalcHeartbeatScorePayload.class); + log.info("CalcHeartbeatScoreHandler payload:{}", payload); + aiUserHeartbeatRelationService.calcAiUserHeartbeatScore(payload.getUserId(),payload.getAiId()); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.CALC_HEARTBEAT_SCORE.getEventCode(), this); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/SubtractHeartbeatValHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/SubtractHeartbeatValHandler.java new file mode 100644 index 0000000..038a6ed --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/handler/SubtractHeartbeatValHandler.java @@ -0,0 +1,40 @@ +package com.sonic.frog.event.inner.handler; + +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.frog.event.inner.EventType; +import com.sonic.frog.event.inner.payload.SubtractHeartbeatValPayload; +import com.sonic.frog.service.AiUserHeartbeatRelationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author code + */ +@Slf4j +@Component +public class SubtractHeartbeatValHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + + @Override + public void onEvent(Event event) { + SubtractHeartbeatValPayload payload = event.normalizedData(SubtractHeartbeatValPayload.class); + log.info("SubtractHeartbeatValHandler payload:{}", payload); + if (payload.getUserId() == null) { + return; + } +// aiUserHeartbeatRelationService.subtractHeartbeatVal(payload.getUserId()); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.SUBTRACT_HEARTBEAT_VAL.getEventCode(), this); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/AiChangePayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/AiChangePayload.java new file mode 100644 index 0000000..9d26e39 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/AiChangePayload.java @@ -0,0 +1,28 @@ +package com.sonic.frog.event.inner.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AiChangePayload { + + /** + * aiId + */ + private Long aiId; + + /** + * 是否修改了对话开场白 + */ + private Boolean isDialoguePrologueChange; + + /** + * 是否是创建 + */ + private Boolean isCreate; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/AiImInfoPayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/AiImInfoPayload.java new file mode 100644 index 0000000..3ce0ee7 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/AiImInfoPayload.java @@ -0,0 +1,20 @@ +package com.sonic.frog.event.inner.payload; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Data +public class AiImInfoPayload { + + private Long aiId; + + private OptType optType; + + + public enum OptType { + ADD, + UPDATE + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/AiUserStatPayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/AiUserStatPayload.java new file mode 100644 index 0000000..ad2ae10 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/AiUserStatPayload.java @@ -0,0 +1,54 @@ +package com.sonic.frog.event.inner.payload; + +import lombok.*; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AiUserStatPayload { + /** + * AI的id + */ + private Long aiId; + + /** + * 类型 + */ + private Type type; + + /** + * ADD_COIN时,才传 + */ + private Long coinNum; + + @Getter + public enum Type { + /** + * 点赞-更新点赞数 + */ + LIKED, + /** + * 取消点赞-更新点赞数 + */ + CANCEL_LIKED, + /** + * 聊天-聊天数更新 + */ + CHAT, + /** + * 首次聊天更新会话数 + */ + FIRST_CHAT, + + /** + * 打赏礼物 + */ + GIFT, + + /** + * 解锁图片 + */ + UNLOCK_IMG, + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/CalcAiUserHeartbeatRankPayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/CalcAiUserHeartbeatRankPayload.java new file mode 100644 index 0000000..afc087b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/CalcAiUserHeartbeatRankPayload.java @@ -0,0 +1,37 @@ +package com.sonic.frog.event.inner.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CalcAiUserHeartbeatRankPayload { + /** + * 用户id + */ + private Long userId; + + /** + * ai id + */ + private Long aiId; + + /** + * 计算前的心动值 + */ + private BigDecimal oldHeartbeatVal; + + /** + * 计算后的心动值 + */ + private BigDecimal newHeartbeatVal; + + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/CalcHeartbeatLevelPayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/CalcHeartbeatLevelPayload.java new file mode 100644 index 0000000..838aba1 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/CalcHeartbeatLevelPayload.java @@ -0,0 +1,65 @@ +package com.sonic.frog.event.inner.payload; + +import com.google.common.collect.Lists; +import lombok.*; + +import java.math.BigDecimal; +import java.util.List; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CalcHeartbeatLevelPayload { + /** + * 用户id + */ + private Long userId; + + /** + * AI的id + */ + private Long aiId; + + /** + * 心动值 + */ + private BigDecimal heartbeatVal; + + /** + * 类型 聊天,24小时未聊天扣减心动值,发送礼物,语音通话 + */ + private Type type; + + /** + * 扩展字段 + */ + private String ext; + + @Getter + public enum Type { + //聊天 + CHAT, + //24小时未聊天扣减心动值 + HOURS_WITHOUT_CHAT, + //发送礼物 + SEND_GIFT, + //语音通话 + VOICE_CHAT, + //购买心动值 + BUY_HEARTBEAT_VAL, + ; + + /** + * 是否聊天类型 + * + * @param type + * @return + */ + public static Boolean isChatType(Type type) { + List chatTypeList = Lists.newArrayList(CHAT, SEND_GIFT, VOICE_CHAT); + return chatTypeList.contains(type); + } + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/CalcHeartbeatScorePayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/CalcHeartbeatScorePayload.java new file mode 100644 index 0000000..233e67b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/CalcHeartbeatScorePayload.java @@ -0,0 +1,23 @@ +package com.sonic.frog.event.inner.payload; + +import com.google.common.collect.Lists; +import lombok.*; + +import java.math.BigDecimal; +import java.util.List; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CalcHeartbeatScorePayload { + /** + * 用户id + */ + private Long userId; + + /** + * AI的id + */ + private Long aiId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/SubtractHeartbeatValPayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/SubtractHeartbeatValPayload.java new file mode 100644 index 0000000..a66b293 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/inner/payload/SubtractHeartbeatValPayload.java @@ -0,0 +1,17 @@ +package com.sonic.frog.event.inner.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class SubtractHeartbeatValPayload { + /** + * 用户id + */ + private Long userId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/EventType.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/EventType.java new file mode 100644 index 0000000..f6b2520 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/EventType.java @@ -0,0 +1,40 @@ +package com.sonic.frog.event.outer; + +import com.sonic.common.event.Event; +import com.sonic.frog.config.EventConfig; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义不属于当前系统的消息 + * @author code + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** 事件定义 */ + USER_CREATED(Event.BuildInScene.BS.getCode(), "bs_user", "user_created", "用户创建"), + + AI_CHAT(EventConfig.DEFAULT_SCENE, EventConfig.PIGEON, "ai_chat", "和AI聊天"), + + USER_DEDUCTION_STAT(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "user_deduction_stat", "文本,语音,语音通话预扣款统计"), + + USER_BALANCE_INSUFFICIENT_CHECKOUT(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "user_balance_insufficient_checkout", "余额不足,文本,语音,语音通话预扣款结算"), + + AI_CHAT_TO_FROG(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "ai_chat_to_frog", "AI聊天同步到业务系统"), + + + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/AiChatNotifyUpdateLastChatTimeHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/AiChatNotifyUpdateLastChatTimeHandler.java new file mode 100644 index 0000000..71dd83e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/AiChatNotifyUpdateLastChatTimeHandler.java @@ -0,0 +1,72 @@ +package com.sonic.frog.event.outer.handler; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.event.outer.EventType; +import com.sonic.frog.event.outer.payload.AiChatPayload; +import com.sonic.frog.service.AiUserService; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +/** + * AI聊天同步业务服务 + * @author code + */ +@Slf4j +@Component +public class AiChatNotifyUpdateLastChatTimeHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private AiUserService aiUserService; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public void onEvent(Event event) { + //处理业务逻辑,更新AI的最后聊天时间 + AiChatPayload payload = event.normalizedData(AiChatPayload.class); + //自己和自己聊不更新最后聊天时间 + boolean isExist = stringRedisTemplate.hasKey(redisKeyUtils.creatorAiUserRelationKey(payload.getFromUserId(), payload.getToUserId())); + if(isExist) { + return; + } + //查询AI + AiUser aiUser = aiUserService.getOne(Wrappers.lambdaQuery().select(AiUser::getId, AiUser::getAiId, AiUser::getUserId).eq(AiUser::getAiId, payload.getToUserId())); + if(aiUser == null) { + return; + } + //自己和自己聊不更新最后聊天时间 + if(aiUser.getUserId().equals(payload.getFromUserId())) { + stringRedisTemplate.opsForValue().set(redisKeyUtils.creatorAiUserRelationKey(payload.getFromUserId(), payload.getToUserId()), "1", 24 * 60 * 60, TimeUnit.SECONDS); + return; + } + //已经更新了则不再继续更新 + boolean isUpdateExist = stringRedisTemplate.hasKey(redisKeyUtils.updateLastChatTimeLimitKey(payload.getToUserId())); + if(isUpdateExist) { + return; + } + //更新数据 + aiUserService.update(Wrappers.lambdaUpdate().set(AiUser::getLastChatTime, LocalDateTime.now()).eq(AiUser::getId, aiUser.getId())); + //写入redis + stringRedisTemplate.opsForValue().set(redisKeyUtils.updateLastChatTimeLimitKey(payload.getToUserId()), "1", 60, TimeUnit.SECONDS); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.AI_CHAT_TO_FROG.getEventCode(), this); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/UserBalanceInsufficientCheckoutHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/UserBalanceInsufficientCheckoutHandler.java new file mode 100644 index 0000000..52870c0 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/UserBalanceInsufficientCheckoutHandler.java @@ -0,0 +1,53 @@ +package com.sonic.frog.event.outer.handler; + +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.event.outer.EventType; +import com.sonic.frog.event.outer.payload.UserBalanceInsufficientCheckoutPayload; +import com.sonic.frog.event.outer.payload.UserDeductionStatPayload; +import com.sonic.frog.service.UserDeductionStatService; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author code + */ +@Slf4j +@Component +public class UserBalanceInsufficientCheckoutHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private UserDeductionStatService userDeductionStatService; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + + @Override + public void onEvent(Event event) { + UserBalanceInsufficientCheckoutPayload payload = event.normalizedData(UserBalanceInsufficientCheckoutPayload.class); + log.info("UserBalanceInsufficientCheckoutHandler payload:{}", payload); + if (payload.getUserId() == null) { + return; + } + //加锁,防止并发统计出问题 + String userBalanceInsufficientCheckoutLockKey = redisKeyUtils.userBalanceInsufficientCheckoutLockKey(payload.getUserId()); + RedisLock redisLock = new RedisLock(userBalanceInsufficientCheckoutLockKey, redisWrapper); + redisLock.tryAcquireRun(() -> { + userDeductionStatService.userDeductionCheckout(payload.getUserId()); + return true; + }); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.USER_BALANCE_INSUFFICIENT_CHECKOUT.getEventCode(), this); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/UserCreatedThenHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/UserCreatedThenHandler.java new file mode 100644 index 0000000..db90cac --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/UserCreatedThenHandler.java @@ -0,0 +1,33 @@ +package com.sonic.frog.event.outer.handler; + +import com.sonic.frog.event.outer.EventType; +import com.sonic.frog.event.outer.payload.UserCratedPayload; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author code + */ +@Slf4j +@Component +public class UserCreatedThenHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + + @Override + public void onEvent(Event event) { + UserCratedPayload payload = event.normalizedData(UserCratedPayload.class); + // TODO: + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.USER_CREATED.getEventCode(), this); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/UserDeductionStatHandler.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/UserDeductionStatHandler.java new file mode 100644 index 0000000..296f303 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/handler/UserDeductionStatHandler.java @@ -0,0 +1,53 @@ +package com.sonic.frog.event.outer.handler; + +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.event.outer.EventType; +import com.sonic.frog.event.outer.payload.UserCratedPayload; +import com.sonic.frog.event.outer.payload.UserDeductionStatPayload; +import com.sonic.frog.service.UserDeductionStatService; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author code + */ +@Slf4j +@Component +public class UserDeductionStatHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private UserDeductionStatService userDeductionStatService; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + + @Override + public void onEvent(Event event) { + UserDeductionStatPayload payload = event.normalizedData(UserDeductionStatPayload.class); + log.info("UserDeductionStatHandler payload:{}", payload); + if (payload.getUserId() == null || payload.getAiId() == null || payload.getDeductionType() == null) { + return; + } + //加锁,防止并发统计出问题 + String userDeductionsStatLockKey = redisKeyUtils.userDeductionsStatLockKey(payload.getUserId(), payload.getAiId(), payload.getDeductionType()); + RedisLock redisLock = new RedisLock(userDeductionsStatLockKey, redisWrapper); + redisLock.tryAcquireRun(() -> { + userDeductionStatService.userDeductionStat(payload.getUserId(), payload.getAiId(), payload.getDeductionType(), payload.getExtra()); + return true; + }); + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.USER_DEDUCTION_STAT.getEventCode(), this); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/AiChatPayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/AiChatPayload.java new file mode 100644 index 0000000..5270c7c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/AiChatPayload.java @@ -0,0 +1,41 @@ +package com.sonic.frog.event.outer.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author coder + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiChatPayload { + + private Long fromUserId; + + private Long toUserId; + + private String content; + + /** + * 消息类型 + */ + private String messageType; + + /** 消息附件 */ + private String attach; + + /** + * 消息来源 + */ + private SourceType sourceType = SourceType.IM; + + public enum SourceType { + IM, + GIFT; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/UserBalanceInsufficientCheckoutPayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/UserBalanceInsufficientCheckoutPayload.java new file mode 100644 index 0000000..062d34f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/UserBalanceInsufficientCheckoutPayload.java @@ -0,0 +1,22 @@ +package com.sonic.frog.event.outer.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author coder + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserBalanceInsufficientCheckoutPayload { + + /** + * 用户id + */ + private Long userId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/UserCratedPayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/UserCratedPayload.java new file mode 100644 index 0000000..b860635 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/UserCratedPayload.java @@ -0,0 +1,17 @@ +package com.sonic.frog.event.outer.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author code + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserCratedPayload { + private Long userId; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/UserDeductionStatPayload.java b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/UserDeductionStatPayload.java new file mode 100644 index 0000000..40390d0 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/event/outer/payload/UserDeductionStatPayload.java @@ -0,0 +1,48 @@ +package com.sonic.frog.event.outer.payload; + +import com.sonic.frog.enums.DeductionTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @author coder + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserDeductionStatPayload { + + /** + * 用户id + */ + private Long userId; + + /** + * AI id + */ + private Long aiId; + + /** + * 扣除类型 + */ + private DeductionTypeEnum deductionType; + + /** + * 业务发生时间 + */ + private LocalDateTime bizTime; + + /** + * 额外扩展字段 json + * 文本:用户内容,ai内容 [{"user":123,"content":"xxxx"},{"ai":222,"content":"xxx"}] + * 语音:发送语音 + * 语音通话:是否结束 {"status":1,"voiceCallId":22, "duration":300} 1: 通话中,2:打断 3:通话结束 + */ + private String extra; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/job/CalcHeartbeatValTotalRankJob.java b/sonic-frog/server/src/main/java/com/sonic/frog/job/CalcHeartbeatValTotalRankJob.java new file mode 100644 index 0000000..54b908d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/job/CalcHeartbeatValTotalRankJob.java @@ -0,0 +1,68 @@ +package com.sonic.frog.job; + + +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.frog.dao.AiUserHeartbeatRankDao; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.DefaultTypedTuple; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * 计算心动总分值榜单 + */ +@Slf4j +@Component +public class CalcHeartbeatValTotalRankJob { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private AiUserHeartbeatRankDao aiUserHeartbeatRankDao; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** 每1小时执行一次 */ + @Scheduled(cron = "0 0 * * * ?") + public void calcHeartbeatValTotalRankSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("calcHeartbeatValTotalRankJob", TimeUnit.MINUTES.toSeconds(2), this::calcHeartbeatValTotalRankJob); + } + + protected JobmanClient.JobResult calcHeartbeatValTotalRankJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> calcHeartbeatValTotalRankJob start !!!"); + //获取榜单的 top 1000出来,放到redis中去 + List userIdList = aiUserHeartbeatRankDao.getAiUserHeartbeatRankList(1000); + Set> tuples = new HashSet<>(); + int i = 1; + for (Long userId : userIdList) { + ZSetOperations.TypedTuple tuple = new DefaultTypedTuple<>(userId.toString(), (double) i); // 修复点:使用 Double 类型 + tuples.add(tuple); + i++; + } + //存储到redis中去 + stringRedisTemplate.opsForZSet().add(redisKeyUtils.heartbeatValTotalRankKey(), tuples); + //设置过期时间 10天 + stringRedisTemplate.expire(redisKeyUtils.heartbeatValTotalRankKey(), 10, TimeUnit.DAYS); + log.info("===> calcHeartbeatValTotalRankJob count : {} end !!!", 0); + return JobmanClient.JobResult.success(0); + } finally { + LogUtils.removeTraceId(); + } + } + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/job/HomeClassificationDefaultConditionCacheJob.java b/sonic-frog/server/src/main/java/com/sonic/frog/job/HomeClassificationDefaultConditionCacheJob.java new file mode 100644 index 0000000..7109a9a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/job/HomeClassificationDefaultConditionCacheJob.java @@ -0,0 +1,55 @@ +package com.sonic.frog.job; + + +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.frog.domain.input.ClassificationListInput; +import com.sonic.frog.service.HomeClassificationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 分类默认条件缓存定时任务 + * 角色-选择所有 + * 性格-选择感性 + * 标签-选择感性下所有标签 + * + * @author mzc + */ +@Slf4j +@Component +public class HomeClassificationDefaultConditionCacheJob { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private HomeClassificationService homeClassificationService; + + /** + * 每5分钟执行一次 + */ + @Scheduled(cron = "0 0/5 * * * ? ") + public void homeClassificationDefaultConditionCacheSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("homeClassificationDefaultConditionCache", TimeUnit.MINUTES.toSeconds(2), this::homeClassificationDefaultConditionCache); + } + + protected JobmanClient.JobResult homeClassificationDefaultConditionCache(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> homeClassificationDefaultConditionCache start !!!"); + //默认ALL,没有任何筛选条件 + ClassificationListInput input = ClassificationListInput.builder() + .build(); + homeClassificationService.homeClassificationUpdateCache(input); + log.info("===> homeClassificationDefaultConditionCache end !!!"); + return JobmanClient.JobResult.success(0); + } finally { + LogUtils.removeTraceId(); + } + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/job/Hours24NoChatSubtractHeartbeatValJob.java b/sonic-frog/server/src/main/java/com/sonic/frog/job/Hours24NoChatSubtractHeartbeatValJob.java new file mode 100644 index 0000000..476623c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/job/Hours24NoChatSubtractHeartbeatValJob.java @@ -0,0 +1,47 @@ +package com.sonic.frog.job; + + +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.frog.service.AiUserHeartbeatRelationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +/** + * 生成图片任务前端心跳过期数据扫描 + * @author mzc + */ +@Slf4j +@Component +public class Hours24NoChatSubtractHeartbeatValJob { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + + /** 每10分钟执行一次 */ +// @Scheduled(cron = "0 0/30 * * * ? ") + @Scheduled(cron = "0/30 * * * * ? ") + public void hours24NoChatSubtractHeartbeatValSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("hours24NoChatSubtractHeartbeatValJob", TimeUnit.MINUTES.toSeconds(2), this::hours24NoChatSubtractHeartbeatValJob); + } + + protected JobmanClient.JobResult hours24NoChatSubtractHeartbeatValJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> hours24NoChatSubtractHeartbeatValJob start !!!"); + aiUserHeartbeatRelationService.hours24NoChatSubtractHeartbeatValJob(); + log.info("===> hours24NoChatSubtractHeartbeatValJob count : {} end !!!", 0); + return JobmanClient.JobResult.success(0); + } finally { + LogUtils.removeTraceId(); + } + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/job/MemberGiftUserCreateCountJob.java b/sonic-frog/server/src/main/java/com/sonic/frog/job/MemberGiftUserCreateCountJob.java new file mode 100644 index 0000000..e87ad1a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/job/MemberGiftUserCreateCountJob.java @@ -0,0 +1,47 @@ +package com.sonic.frog.job; + + +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.frog.service.UserCreateCountStatService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 会员每隔一月赠送用户相册创作次数 + * + * @author mzc + */ +@Slf4j +@Component +public class MemberGiftUserCreateCountJob { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private UserCreateCountStatService userCreateCountStatService; + + /** + * 每5分钟执行一次 + */ + @Scheduled(cron = "0 0/5 * * * ? ") + public void memberGiftUserCreateCountJobSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("memberGiftUserCreateCountJob", TimeUnit.MINUTES.toSeconds(2), this::memberGiftUserCreateCountJob); + } + + protected JobmanClient.JobResult memberGiftUserCreateCountJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> updateAiUserChatNumJob start !!!"); + userCreateCountStatService.giftMemberNumJob(); + return JobmanClient.JobResult.success(0); + } finally { + LogUtils.removeTraceId(); + } + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/job/TextOrVoiceMinute10NoChatCheckoutJob.java b/sonic-frog/server/src/main/java/com/sonic/frog/job/TextOrVoiceMinute10NoChatCheckoutJob.java new file mode 100644 index 0000000..8bf4694 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/job/TextOrVoiceMinute10NoChatCheckoutJob.java @@ -0,0 +1,48 @@ +package com.sonic.frog.job; + + +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.frog.service.UserDeductionStatService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 文本聊天或语音聊天超过10分钟未聊天发起扣费,生成支付流水 + * + * @author mzc + */ +@Slf4j +@Component +public class TextOrVoiceMinute10NoChatCheckoutJob { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private UserDeductionStatService userDeductionStatService; + + /** + * 每1分钟执行一次 + */ + @Scheduled(cron = "0 0/1 * * * ? ") + public void textOrVoiceMinute10NoChatCheckoutJobSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("textOrVoiceMinute10NoChatCheckoutJob", TimeUnit.MINUTES.toSeconds(2), this::textOrVoiceMinute10NoChatCheckoutJob); + } + + protected JobmanClient.JobResult textOrVoiceMinute10NoChatCheckoutJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> textOrVoiceMinute10NoChatCheckoutJob start !!!"); + userDeductionStatService.textOrVoiceTypeNoChatCheckoutJob(); + log.info("===> textOrVoiceMinute10NoChatCheckoutJob count : {} end !!!", 0); + return JobmanClient.JobResult.success(0); + } finally { + LogUtils.removeTraceId(); + } + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/job/UpdateAiUserChatNumJob.java b/sonic-frog/server/src/main/java/com/sonic/frog/job/UpdateAiUserChatNumJob.java new file mode 100644 index 0000000..e272a86 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/job/UpdateAiUserChatNumJob.java @@ -0,0 +1,73 @@ +package com.sonic.frog.job; + + +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.frog.service.AiUserHeartbeatRelationService; +import com.sonic.frog.service.impl.AiUserStatServiceImpl; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * 聊天次数未达到20次的定时刷新到数据库 + * + * @author mzc + */ +@Slf4j +@Component +public class UpdateAiUserChatNumJob { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private AiUserStatServiceImpl aiUserStatService; + + /** + * 每5分钟执行一次 + */ + @Scheduled(cron = "0 0/5 * * * ? ") + public void updateAiUserChatNumJobSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("updateAiUserChatNumJob", TimeUnit.MINUTES.toSeconds(2), this::updateAiUserChatNumJob); + } + + protected JobmanClient.JobResult updateAiUserChatNumJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> updateAiUserChatNumJob start !!!"); + //从zset中取数据 + String aiChatNumKey = redisKeyUtils.aiChatNumKey(); + Set> typedTuples = stringRedisTemplate.opsForZSet().rangeWithScores(aiChatNumKey, 0, -1); + log.info("===> updateAiUserChatNumJob typedTuples:{}", JSONObject.toJSONString(typedTuples)); + for (ZSetOperations.TypedTuple typedTuple : typedTuples) { + //聊天数 + Double score = typedTuple.getScore(); + Integer chatNum = score != null ? Double.valueOf(score).intValue() : 0; + //Ai的Id + Long aiId = Long.valueOf(typedTuple.getValue()); + //刷新到数据库 + aiUserStatService.increaseChatNum(aiId, chatNum); + //刷新完后,删除该ai的聊天数量 + stringRedisTemplate.opsForZSet().remove(aiChatNumKey, aiId.toString()); + } + log.info("===> updateAiUserChatNumJob end !!!"); + return JobmanClient.JobResult.success(0); + } finally { + LogUtils.removeTraceId(); + } + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/job/UpdateHomeRecommendJob.java b/sonic-frog/server/src/main/java/com/sonic/frog/job/UpdateHomeRecommendJob.java new file mode 100644 index 0000000..83ca7d1 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/job/UpdateHomeRecommendJob.java @@ -0,0 +1,48 @@ +package com.sonic.frog.job; + + +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.frog.service.HomeRecommendV2Service; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 更新首页聚合推荐缓存数据 + */ +@Slf4j +@Component +public class UpdateHomeRecommendJob { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private HomeRecommendV2Service homeRecommendV2Service; + + /** 每1分钟执行一次 */ + @Scheduled(cron = "0 0/1 * * * ?") + public void updateHomeRecommendSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("updateHomeRecommendJob", TimeUnit.MINUTES.toSeconds(2), this::updateHomeRecommendJob); + } + + protected JobmanClient.JobResult updateHomeRecommendJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> updateHomeRecommendJob start !!!"); + homeRecommendV2Service.updateCache(); + log.info("===> updateHomeRecommendJob count : {} end !!!", 0); + return JobmanClient.JobResult.success(0); + } catch (Exception e) { + log.error("===> updateHomeRecommendJob error : ", e); + }finally { + LogUtils.removeTraceId(); + } + return JobmanClient.JobResult.success(0); + } + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/job/UpdateRankJob.java b/sonic-frog/server/src/main/java/com/sonic/frog/job/UpdateRankJob.java new file mode 100644 index 0000000..5168f7a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/job/UpdateRankJob.java @@ -0,0 +1,84 @@ +package com.sonic.frog.job; + + +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.frog.service.RankService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 更新榜单相关的定时任务 + */ +@Slf4j +@Component +public class UpdateRankJob { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private RankService rankService; + + /** 每5分钟执行一次 */ + @Scheduled(cron = "0 0/5 * * * ?") + public void chatRankSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("chatRankJob", TimeUnit.MINUTES.toSeconds(2), this::chatRankJob); + } + + protected JobmanClient.JobResult chatRankJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> chatRankJob start !!!"); + int size = rankService.chatRankJob(); + log.info("===> chatRankJob count : {} end !!!", size); + return JobmanClient.JobResult.success(size); + } finally { + LogUtils.removeTraceId(); + } + } + + /** 每5分钟执行一次 */ + @Scheduled(cron = "0 0/5 * * * ?") + public void aiHeartbeatRankSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("aiHeartbeatRankJob", TimeUnit.MINUTES.toSeconds(2), this::aiHeartbeatRankJob); + } + + protected JobmanClient.JobResult aiHeartbeatRankJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> aiHeartbeatRankJob start !!!"); + int size = rankService.aiHeartbeatRankJob(); + log.info("===> aiHeartbeatRankJob count : {} end !!!", size); + return JobmanClient.JobResult.success(size); + } finally { + LogUtils.removeTraceId(); + } + } + + + /** 每5分钟执行一次 */ + @Scheduled(cron = "0 0/5 * * * ?") + public void aiGiftRankSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("aiGiftRankJob", TimeUnit.MINUTES.toSeconds(2), this::aiGiftRankJob); + } + + protected JobmanClient.JobResult aiGiftRankJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> aiGiftRankJob start !!!"); + int size = rankService.giftRankJob(); + log.info("===> aiGiftRankJob count : {} end !!!", size); + return JobmanClient.JobResult.success(size); + } finally { + LogUtils.removeTraceId(); + } + } + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/limit/RequestLimit.java b/sonic-frog/server/src/main/java/com/sonic/frog/limit/RequestLimit.java new file mode 100644 index 0000000..d6f546c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/limit/RequestLimit.java @@ -0,0 +1,45 @@ +package com.sonic.frog.limit; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.*; + +/** + * @description: 限流注解 + * @author: code + **/ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +@Order(Ordered.HIGHEST_PRECEDENCE) +public @interface RequestLimit { + + /** + * 允许访问的最大次数 + */ + int count() default Integer.MAX_VALUE; + + /** + * 已登录用户允许访问的最大次数(备注:如果不配置值的话默认都走IP的限制) + * @return + */ + int loginCount() default Integer.MAX_VALUE; + + /** + * 时间段,单位为毫秒,默认值一分钟 + */ + long time() default 60000; + + /** + * 未登录请求用户达到限流时的提示 异常码 默认值为:1001009 / 如果想要未登录用户跳转到登录页面则返回异常码为 noLoginErrorCode = "10050001" + * @return + */ + String noLoginErrorCode() default "1001009"; + + /** + * message 提示文案 + * @return + */ + String message() default "Sorry to detect your abnormal access, please try again later"; +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/limit/RequestLimitContract.java b/sonic-frog/server/src/main/java/com/sonic/frog/limit/RequestLimitContract.java new file mode 100644 index 0000000..974037c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/limit/RequestLimitContract.java @@ -0,0 +1,109 @@ +package com.sonic.frog.limit; + + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.enums.AppEnv; +import com.sonic.common.utils.IpAddressUtils; +import com.sonic.frog.enums.BizResultCode; +import com.sonic.frog.enums.BusinessException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 限流控制器 + */ +@Order(99) +@Slf4j +@Aspect +@Component +public class RequestLimitContract { + + @Value("${spring.profiles.active}") + private String runMode; + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * redis key不存在的过期时间值 + */ + private final Integer NULL_KEY_EXP_TIME_VALUE = -2; + + /** + * redis key永不过期的过期时间值 + */ + private final Integer NEVER_EXPIRES_VALUE = -1; + + /** + * 默认的异常码 + */ + private static final String DEFAULT_ERROR_CODE = "1999999"; + + @Before("execution(public * com.sonic.frog.controller..*.*(..)) && @annotation(limit)") + public void requestLimit(final JoinPoint joinPoint, RequestLimit limit) { + //dev环境直接放行不做拦截 + if (StringUtils.isNotBlank(runMode) && AppEnv.dev.name().equals(runMode)) { + return; + } + Object[] args = joinPoint.getArgs(); + HttpServletRequest request = null; + Session session = null; + for (Object arg : args) { + //解析方法的HttpServletRequest入参对象 + if (arg instanceof HttpServletRequest) { + request = (HttpServletRequest) arg; + } + //解析方法的Session入参对象、必须要配置了登录次数限制的才能进行解析 + if (arg instanceof Session && limit.loginCount() != Integer.MAX_VALUE) { + session = (Session) arg; + } + } + //判断请求是否为空,是否需要抛出异常 + BizResultCode.MISS_PARAM_ERROR.check(request == null); + int num = 0; + String ipOrUserId = null; + Integer limitCount = null; + String url = null; + boolean loginUserBl = false; + try { + ipOrUserId = (session == null || session.getUserId() == null) ? IpAddressUtils.getIpAddress(request) : session.getUserId().toString(); + limitCount = (session == null || session.getUserId() == null) ? limit.count() : limit.loginCount(); + loginUserBl = session != null && session.getUserId() != null; + url = request.getRequestURI(); + //eg: limit:path:/mobile/third/login:127-0-0-1 + String key = "limit:path:".concat(url).concat(":").concat(StringUtils.isNotEmpty(ipOrUserId) ? ipOrUserId.replace(":", "-") : ipOrUserId); + //处理限流为-1的情况 + Long expTime = stringRedisTemplate.getExpire(key); + if (expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().set(key, "1", limit.time(), TimeUnit.MILLISECONDS); + } else { + num = Objects.requireNonNull(stringRedisTemplate.opsForValue().increment(key, 1)).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis的key + stringRedisTemplate.delete(key); + } + } catch (Exception e) { + log.error("requestLimit error", e); + } + if (num > limitCount) { + log.info("===> 限流触发,访问地址:{}, 用户信息:{}, 限定的次数:{}", ipOrUserId, url, limit.count()); + //未登录的用户达到限流时如果配置了跳转登录页的errorCode的话前端会直接去跳转登录页面 + throw new BusinessException(loginUserBl ? DEFAULT_ERROR_CODE : limit.noLoginErrorCode(), limit.message()); + } + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AdvertiseService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AdvertiseService.java new file mode 100644 index 0000000..40f1f09 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AdvertiseService.java @@ -0,0 +1,28 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.Advertise; +import com.sonic.frog.domain.output.AdvertiseOutput; +import com.sonic.frog.domain.output.AiCarouselListOutput; + +import java.util.List; + +/** + * 广告管理 Service + */ +public interface AdvertiseService extends IService { + + /** + * 获取广告列表 + * + * @return + */ + List getAdvertiseList(Long userId); + + /** + * 获取AI轮播列表 + * + * @return + */ + List getAiCarouselList(Long userId); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiChatInfoService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiChatInfoService.java new file mode 100644 index 0000000..1883a77 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiChatInfoService.java @@ -0,0 +1,15 @@ +package com.sonic.frog.service; + +import com.sonic.frog.domain.output.AiChatInfoOutput; + +public interface AiChatInfoService { + + /** + * 获取AI与用户的聊天信息 + * @param userId + * @param aiId + * @return + */ + AiChatInfoOutput getAiChatInfo(Long userId, Long aiId); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiDictService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiDictService.java new file mode 100644 index 0000000..b1f5e20 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiDictService.java @@ -0,0 +1,35 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.AiDict; +import com.sonic.frog.domain.output.AiDictOut; + +import java.util.Map; +import java.util.Set; + +/** + *

+ * AI角色,性格,标签,形象风格字典表 服务类 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +public interface AiDictService extends IService { + + /** + * AI角色,性格,标签,形象风格字典 + * + * @return + */ + AiDictOut getAiDict(); + + /** + * 批量获取名称 + * + * @param codeList + * @return + */ + Map mapNameByCodeList(Set codeList); +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserAlbumService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserAlbumService.java new file mode 100644 index 0000000..0a0c112 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserAlbumService.java @@ -0,0 +1,122 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.common.rpc.Page; +import com.sonic.frog.domain.entity.AiUserAlbum; +import com.sonic.frog.domain.input.*; +import com.sonic.frog.domain.output.AiAlbumDetailOutput; +import com.sonic.frog.domain.output.ListAiAlbumOutput; +import com.sonic.frog.domain.output.ViewUnlockAlbumImgOutput; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; + +import java.security.InvalidKeyException; +import java.util.List; + +/** + * AI用户相册业务接口 + */ +public interface AiUserAlbumService extends IService { + + /** + * 批量插入返回主键id列表 + * + * @param currentUserId + * @param addAlbum + * @return + */ + List batchAddAlbum(Long currentUserId, BatchAddAlbumInput addAlbum); + + /** + * 设置默认图片 + * + * @param currentUserId + * @param aiId + * @param albumId + */ + void setDefaultAlbum(Long currentUserId, Long aiId, Long albumId); + + /** + * 相册删除 + * + * @param currentUserId + * @param albumId + */ + void delAlbum(Long currentUserId, Long albumId); + + /** + * 根据aiID删除用户相册 + * + * @param aiId + */ + void delByAiId(Long aiId); + + /** + * 点赞相册图片 + * + * @param currentUserId + * @param input + */ + void likeOrCancelPic(Long currentUserId, LikeOrCancelPicInput input); + + /** + * 相册列表 + * + * @param input + * @param currentUserId + * @return + */ + Page listAlbums(AlbumListInput input, Long currentUserId); + + /** + * 相册列表 + * @param currentUserId + * @param idList + * @return + */ + List listByIds(Long currentUserId, List idList); + + /** + * 添加Ai形象图或编辑形象图时,自动添加一张默认图片到Ai相册中 + */ + void addDefaultAlbum(Long currentUserId, Long aiId, String imageUrl, String width, String height); + + /** + * 设置相册解锁价格 + * + * @param currentUserId + * @param input + */ + void setAlbumUnlockPrice(Long currentUserId, SetAlbumUnlockPriceInput input); + + /** + * 解锁加密图片 + * + * @param userId + * @param input + */ + ViewUnlockAlbumImgOutput unlockAlbumImg(Long userId, UnlockAlbumImgInput input); + + /** + * 获取加密图片解锁后访问 + * + * @param userId + * @param input + * @return + */ + ViewUnlockAlbumImgOutput viewUnlockAlbumImg(Long userId, ViewUnlockAlbumImgInput input); + + /** + * 随机获取一张未解锁的图片 + * @param input + * @return + */ + AIUserAlbumApiOutput getRandomLockImage(GetRandomLockImageInput input) throws InvalidKeyException; + + /** + * 秘密爱慕者请求的相册详情信息 + * @param albumId + * @return + */ + AiAlbumDetailOutput rcDetail(Long albumId) throws InvalidKeyException; + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserAlbumUnlockService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserAlbumUnlockService.java new file mode 100644 index 0000000..c9f9451 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserAlbumUnlockService.java @@ -0,0 +1,63 @@ +package com.sonic.frog.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.AiUserAlbum; +import com.sonic.frog.domain.entity.AiUserAlbumUnlock; +import com.sonic.frog.enums.LockStatusEnum; + +import java.util.List; +import java.util.Map; + +/** + * @Author zzhan + * @Description 查询相册图片是否对访问用户解锁 + * @Date 2023/8/28 13:57 + * @Version 1.0 + */ +public interface AiUserAlbumUnlockService extends IService { + + + /** + * 批量查询相册解锁 + * + * @param currentUserId + * @param aiUserAlbumList ai相册列表 + * @return + */ + Map queryAlbumUnlock(Long currentUserId, List aiUserAlbumList); + + /** + * 校验相册是否解锁 + * + * @param currentUserId + * @param aiUserAlbum + */ + void checkAlbumLockStatus(Long currentUserId, AiUserAlbum aiUserAlbum); + + /** + * 查询当前用户解锁过哪些相册图片 + * + * @param currentUserId + * @return + */ + List getUnlockAlbumIds(Long currentUserId); + + /** + * 添加解锁记录 + * + * @param currentUserId + * @param albumId + * @param orderNo + */ + void addUnlockAlbumRecord(Long currentUserId, Long albumId, String orderNo); + + /** + * 用户是否解锁该相册id + * + * @param currentUserId + * @param albumId + * @return + */ + Boolean isUnlockAlbumId(Long currentUserId, Long albumId); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserExtService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserExtService.java new file mode 100644 index 0000000..ad68820 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserExtService.java @@ -0,0 +1,55 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.AiUserExt; +import com.sonic.frog.domain.input.AiUserExtInput; + +import java.util.List; + +/** + * AI用户扩展表服务接口 + */ +public interface AiUserExtService extends IService { + + + /** + * 保存或更新AI用户扩展信息 + * + * @param aiId + * @param aiUserExt + * @param input + * @param currentUserId + */ + void saveOrUpdateAiUserExt(Long aiId, AiUserExt aiUserExt, AiUserExtInput input, Long currentUserId); + + /** + * 获取AI用户扩展信息 + * + * @param aiId + * @return + */ + AiUserExt getAiUserExtByAiId(Long aiId); + + /** + * 删除AI用户 + * + * @param aiId + */ + void delAiUser(Long aiId); + + /** + * 更新AI用户开场白语音 + * + * @param url + * @param aiId + */ + void updateAiUserDialoguePrologueSound(String url, Long aiId); + + /** + * 更新AI辅助聊天内容 + * + * @param supContentList + * @param aiId + */ + void updateAiUserSupportingContent(List supContentList, Long aiId); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserGiftService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserGiftService.java new file mode 100644 index 0000000..5e08bec --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserGiftService.java @@ -0,0 +1,32 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.common.rpc.Page; +import com.sonic.frog.domain.entity.AiUserGift; +import com.sonic.frog.domain.input.AiUserGiftListInput; +import com.sonic.frog.domain.input.SendGiftInput; +import com.sonic.frog.domain.output.AiUserGiftListOutput; + +/** + * AI用户收到礼物业务接口 + */ +public interface AiUserGiftService extends IService { + + /** + * 获取AI用户礼物列表 + * + * @param input + * @return + */ + Page getAiUserGiftList(AiUserGiftListInput input); + + /** + * 用户发送礼物 + * + * @param currentUserId + * @param input + */ + void sendGift(Long currentUserId, SendGiftInput input); + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserHeartbeatRankService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserHeartbeatRankService.java new file mode 100644 index 0000000..586e1eb --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserHeartbeatRankService.java @@ -0,0 +1,24 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.AiUserHeartbeatRank; + +import java.math.BigDecimal; + +public interface AiUserHeartbeatRankService extends IService { + + /** + * 增加或扣减心动值 + * @param userId + * @param heartbeatVal + */ + void incrementHeartbeatVal(Long userId, BigDecimal heartbeatVal); + + /** + * 获取当前用户的榜单数据 + * @param userId + * @return + */ + BigDecimal getCurrentUserRank(Long userId); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserHeartbeatRelationService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserHeartbeatRelationService.java new file mode 100644 index 0000000..3360504 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserHeartbeatRelationService.java @@ -0,0 +1,145 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.common.rpc.Page; +import com.sonic.frog.domain.entity.AiUserHeartbeatRelation; +import com.sonic.frog.domain.input.BuyHeartbeatValInput; +import com.sonic.frog.domain.input.HeartbeatRelationListInput; +import com.sonic.frog.domain.input.HeartbeatRelationSwitchInput; +import com.sonic.frog.domain.output.AiUserHeartbeatRelationOutput; +import com.sonic.frog.domain.output.HeartbeatLevelOutput; +import com.sonic.frog.domain.output.HeartbeatRelationListOutput; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import com.sonic.frog.event.inner.payload.CalcHeartbeatLevelPayload; + +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 用户与AI的心动关系服务接口 + */ +public interface AiUserHeartbeatRelationService extends IService { + + /** + * 获取用户与ai的心动关系 + * + * @param userId + * @param aiId + * @return + */ + AiUserHeartbeatRelation getHeartbeatRelation(Long userId, Long aiId); + + /** + * 初始化用户与ai的心动关系 + * + * @param userId + * @param aiId + */ + void initAiUserHeartbeatRelation(Long userId, Long aiId); + + /** + * 获取用户与ai的心动关系 + * + * @param userId + * @param aiId + * @return + */ + AiUserHeartbeatRelationOutput getAiUserHeartbeatRelation(Long userId, Long aiId); + + /** + * 获取用户与ai的心动等级 + * + * @param userId + * @param aiId + * @return + */ + HeartbeatLevelOutput getAiUserHeartbeatLevel(Long userId, @NotNull Long aiId); + + /** + * 获取用户与ai的心动等级枚举 + * + * @param userId + * @param aiId + * @return + */ + HeartbeatLevelEnum getHeartbeatLevel(Long userId, Long aiId); + + /** + * 计算用户与ai的心动等级 + * + * @param payload + */ + void calcAiUserHeartbeatLevel(CalcHeartbeatLevelPayload payload); + + /** + * 购买心动值-已扣减的心动值购买 + * + * @param userId + * @param input + */ + void buyHeartbeatVal(Long userId, BuyHeartbeatValInput input); + + /** + * 用户与ai的心动关系开关 + * + * @param userId + * @param input + */ + void heartbeatRelationSwitch(Long userId, HeartbeatRelationSwitchInput input); + + /** + * 计算用户与ai的心动分 + * + * @param userId + * @param aiId + */ + void calcAiUserHeartbeatScore(Long userId, Long aiId); + + /** + * 查询当前用户下所有AI,如果聊天超过24小时未聊天则扣减心动值 + * + * @param userId + */ + void subtractHeartbeatVal(Long userId); + + /** + * 24小时未聊天扣减心动值 + * + * @param userId + */ + void withoutChatSubtractHeartbeatVal(Long userId); + + /** + * 心动关系列表 + * + * @param userId + * @param input + * @return + */ + Page heartbeatRelationList(Long userId, HeartbeatRelationListInput input); + + /** + * 24小时未聊天扣减心动值定时任务 + */ + void hours24NoChatSubtractHeartbeatValJob(); + + /** + * 查询用户和目标用户的心动值 + * + * @param currentUserId + * @param aiIds + * @return + */ + Map queryAIHeartbeatVal(Long currentUserId, List aiIds); + + /** + * 是否存在关系校验 + * + * @param userId + * @param aiId + * @return + */ + Boolean relationCheck(Long userId, Long aiId); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserSearchService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserSearchService.java new file mode 100644 index 0000000..eb5e2eb --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserSearchService.java @@ -0,0 +1,98 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.output.*; + +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +/** + * AI用户表服务接口 + */ +public interface AiUserSearchService extends IService { + + /** + * 根据aiId查询Ai用户信息 个人主页用 + * + * @param currentUserId + * @param aiId + * @return + */ + AiUserBaseOutput getAiUserBaseInfo(Long currentUserId, Long aiId); + + + /** + * 获取自己的AI用户列表 + * + * @param userId + * @return + */ + List getBaseUserAiList(Long userId); + + /** + * 获取别人的AI用户列表 + * + * @param aiId + * @return + */ + List getTargetUserAiList(Long aiId); + + /** + * 批量获取Ai字典 + * + * @param list + * @return + */ + Map mapNameByCodeList(List list); + + /** + * 获取AI用户H5信息 + * + * @param aiId + * @return + */ + AiUserH5Output getAiUserH5Info(Long aiId); + + /** + * 根据AI的id获取AI用户信息 + * + * @param aiId + * @return + */ + AiUser getAiUserByAiId(Long aiId); + + /** + * 获取im聊天ai用户基础信息 + * + * @param aiId + * @param userId + * @return + */ + AiUserImBaseInfoOutput getAiUserImBaseInfo(Long aiId, Long userId); + + /** + * 批量获取Ai用户信息 + * + * @param aiIdList + * @return + */ + Map mapByAiIdList(List aiIdList); + + /** + * 获取用户AI数量 + * + * @param userId + * @return + */ + Integer countAiNumByUserId(Long userId); + + /** + * 获取AI用户seo基础信息 + * + * @param aiId + * @return + */ + AiUserSeoBaseInfoOutput getAiUserSeoBaseInfo(Long aiId); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserService.java new file mode 100644 index 0000000..09991d3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserService.java @@ -0,0 +1,20 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.output.AiInfoApiOutput; + +/** + * AI用户表服务接口 + */ +public interface AiUserService extends IService { + + + /** + * 获取AI的基础信息 + * @param aiId + * @return + */ + AiInfoApiOutput getAiUserInfo(Long aiId); + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserSetService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserSetService.java new file mode 100644 index 0000000..68457b3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserSetService.java @@ -0,0 +1,56 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.input.CreateEditAiUserInput; +import com.sonic.frog.domain.input.EditAiHeadImgInput; +import com.sonic.frog.domain.output.AiUserInfoOutput; +import com.sonic.frog.domain.output.CreateEditAiUserOutput; + +import javax.validation.Valid; + +/** + * AI用户表服务接口 + */ +public interface AiUserSetService extends IService { + + /** + * 创建或编辑Ai用户 + * + * @param input + * @param currentUserId + */ + CreateEditAiUserOutput createEditAiUser(CreateEditAiUserInput input, Long currentUserId); + + /** + * 删除Ai用户 + * + * @param aiId + * @param currentUserId + */ + void delAiUser(Long aiId, Long currentUserId); + + /** + * 根据aiId查询我的Ai用户信息 编辑用 + * + * @param aiId + * @param currentUserId + */ + AiUserInfoOutput getMyAiUserInfo(Long aiId, Long currentUserId); + + /** + * 设置ai主页头图 + * + * @param aiId + * @param homeImageUrl + */ + void setHomeImageUrl(Long aiId, String homeImageUrl); + + /** + * 修改ai头像 + * + * @param userId + * @param input + */ + void editAiHeadImg(Long userId, @Valid EditAiHeadImgInput input); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserStatService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserStatService.java new file mode 100644 index 0000000..bff8d0b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/AiUserStatService.java @@ -0,0 +1,92 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.AiUserStat; +import com.sonic.frog.domain.output.AiUserStatOutput; +import com.sonic.frog.event.inner.payload.AiUserStatPayload; + +import java.util.List; +import java.util.Map; + +/** + * AI用户统计业务接口 + */ +public interface AiUserStatService extends IService { + + /** + * 获取Ai用户统计数据 + * + * @param aiId + * @return + */ + AiUserStatOutput getAiUserStatByAiId(Long aiId, Long currentUserId); + + /** + * ai被创建时,初始化Ai用户统计数据 + * + * @param aiId + */ + void initAiUserStat(Long aiId); + + /** + * AI用户统计-MQ消费 + * + * @param payload + */ + void aiUserStat(AiUserStatPayload payload); + + /** + * 用户点赞或取消点赞时,更新Ai用户点赞数 + * + * @param aiId + * @param num 取消点赞为-1,点赞为1 + */ + void updateLikeNum(Long aiId, Integer num); + + /** + * 每20轮对话时,增加Ai用户聊天数 + * + * @param aiId + */ + void increaseChatNum(Long aiId); + + /** + * 定时任务更新未达到20次时,增加Ai用户聊天数 + * + * @param aiId + * @param num + */ + void increaseChatNum(Long aiId, Integer num); + + + /** + * 新用户与AI聊天时,增加与Ai聊过天的会话数 + * + * @param aiId + */ + void increaseConversationNum(Long aiId); + + /** + * 礼物打赏或其他场景 增加Ai用户金币数 Crush Coin数需要扣除平台服务费用 + * + * @param aiId + * @param coinNum + */ + void increaseCoinNum(Long aiId, Long coinNum); + + /** + * 批量查询Ai用户点赞数 + * + * @param aiIds + * @return + */ + Map queryAiLikedCount(List aiIds); + + /** + * 获取Ai用户点赞数 + * + * @param aiId + * @return + */ + Integer getAiLikedCount(Long aiId); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/BuyCreateCountRecordService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/BuyCreateCountRecordService.java new file mode 100644 index 0000000..2935c42 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/BuyCreateCountRecordService.java @@ -0,0 +1,18 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.BuyCreateCountRecord; + +/** + * 用户购买相册创作次数记录 Service + */ +public interface BuyCreateCountRecordService extends IService { + /** + * 保存购买记录 + * + * @param userId + * @param totalAmount + * @param count + */ + void add(Long userId, Long totalAmount, Integer count,String orderNo); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/BuyHeartbeatValueRecordService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/BuyHeartbeatValueRecordService.java new file mode 100644 index 0000000..d59c303 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/BuyHeartbeatValueRecordService.java @@ -0,0 +1,17 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.BuyHeartbeatValueRecord; + +/** + * 用户购买心动值记录服务接口 + */ +public interface BuyHeartbeatValueRecordService extends IService { + + /** + * 添加用户购买心动值记录 + * + * @param buyHeartbeatValueRecord 用户购买心动值记录 + */ + void addBuyHeartbeatValueRecord(BuyHeartbeatValueRecord buyHeartbeatValueRecord); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatBubbleDictService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatBubbleDictService.java new file mode 100644 index 0000000..18b03c4 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatBubbleDictService.java @@ -0,0 +1,40 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.ChatBubbleDict; + +/** + * 聊天气泡字典服务接口 + */ +public interface ChatBubbleDictService extends IService { + /** + * 获取聊天气泡 + * + * @param chatBubbleCode + * @return + */ + ChatBubbleDict getChatBubbleDictByCode(String chatBubbleCode); + + /** + * 获取聊天气泡名称 + * + * @param bubbleCode + * @return + */ + String getBubbleNameByCode(String bubbleCode); + + /** + * 获取默认聊天气泡 + * + * @return + */ + ChatBubbleDict getDefaultChatBubble(); + + /** + * 获取用户的聊天气泡 + * + * @param userSetBubbleCode + * @return + */ + ChatBubbleDict getUserChatBubble(Long userId, Long aiId, String userSetBubbleCode); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatModelDictService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatModelDictService.java new file mode 100644 index 0000000..169d92c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatModelDictService.java @@ -0,0 +1,34 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.ChatModelDict; +import com.sonic.frog.domain.output.ChatModelDictOutput; + +import java.util.List; + +/** + * 对话模型字典服务接口 + */ +public interface ChatModelDictService extends IService { + /** + * 获取对话模型字典列表 + * + * @return + */ + List getChatModelDictList(); + + /** + * 根据模型code获取模型名称 + * + * @param modelCode + * @return + */ + String getModelNameByCode(String modelCode); + + /** + * 获取默认对话模型 + * + * @return + */ + ChatModelDict getDefaultChatModel(); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatService.java new file mode 100644 index 0000000..16d2959 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatService.java @@ -0,0 +1,26 @@ +package com.sonic.frog.service; + +/** + * @description: + * @author: mzc + * @date: 2025-08-29 17:52 + **/ +public interface ChatService { + + /** + * 发送开场白消息 + * + * @param aiId + * @param userId + */ + void sendDialoguePrologueMessage(Long aiId, Long userId); + + /** + * 是否聊过天 + * + * @param aiId + * @param userId + * @return + */ + Boolean isHaveChatted(Long aiId, Long userId); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatSetService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatSetService.java new file mode 100644 index 0000000..5d0190b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatSetService.java @@ -0,0 +1,107 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.ChatSet; +import com.sonic.frog.domain.input.*; +import com.sonic.frog.domain.output.ChatBubbleListOutput; +import com.sonic.frog.domain.output.ChatSetOutput; + +import java.util.List; + +/** + * 用户与Ai的聊天设定表服务接口 + */ +public interface ChatSetService extends IService { + + /** + * 获取聊天设置 + * + * @param userId + * @param aiId + * @return + */ + ChatSet getChatSet(Long userId, Long aiId); + + /** + * 获取我的聊天设置 + * + * @param userId + * @param aiId + * @return + */ + ChatSetOutput getMyChatSet(Long userId, Long aiId); + + /** + * 设置我的聊天设定 + * + * @param userId + * @param input + */ + void setMyChatSetting(Long userId, SetMyChatSettingInput input); + + /** + * 设置对话模型 + * + * @param userId + * @param input + */ + void setChatModel(Long userId, SetChatModelInput input); + + /** + * 使用或取消使用聊天气泡 + * + * @param userId + * @param input + */ + void setChatBubble(Long userId, SetChatBubbleInput input); + + /** + * 设置用户与AI聊天背景图 + * + * @param userId + * @param aiId + * @param imgUrl + */ + void setBackground(Long userId, Long aiId, String imgUrl); + + /** + * 设置是否自动播放语音 + * + * @param userId + * @param input + */ + void setIsAutoPlayVoice(Long userId, SetIsAutoPlayVoiceInput input); + + /** + * 获取聊天气泡列表 + * + * @param userId + * @param input + * @return + */ + List getChatBubbleList(Long userId, ChatBubbleListInput input); + + /** + * 获取用户与AI聊天背景图 + * + * @param userId + * @param aiId + * @return + */ + String getChatSetBackground(Long userId, Long aiId); + + /** + * 更新是否删除聊天记录 + * + * @param userId + * @param aiIdList + * @return + */ + void updateIsDelChatted(Long userId, List aiIdList); + + /** + * 关闭自动播放语音 + * @param userIdList + */ + void closeAutoPlayVoice(List userIdList); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatUserBackgroundService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatUserBackgroundService.java new file mode 100644 index 0000000..5efd4b3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/ChatUserBackgroundService.java @@ -0,0 +1,51 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.ChatUserBackground; +import com.sonic.frog.domain.input.BatchAddBackgroundInput; +import com.sonic.frog.domain.input.SetBackgroundInput; +import com.sonic.frog.domain.output.BackgroundImgListOutput; + +import java.util.List; + +/** + * 对话用户聊天背景服务接口 + */ +public interface ChatUserBackgroundService extends IService { + + /** + * 批量插入返回主键id列表 + * + * @param currentUserId + * @param input + * @return + */ + List batchAddBackground(Long currentUserId, BatchAddBackgroundInput input); + + /** + * 获取用户背景图片列表 + * + * @param userId + * @param aiId + * @return + */ + List getBackgroundImgList(Long userId, Long aiId); + + + /** + * 设置或取消默认图片 + * + * @param currentUserId + * @param input + */ + void setBackground(Long currentUserId, SetBackgroundInput input); + + /** + * 背景删除 + * + * @param currentUserId + * @param backgroundId + */ + void delBackground(Long currentUserId, Long backgroundId); + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/CommonMessageService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/CommonMessageService.java new file mode 100644 index 0000000..47448cd --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/CommonMessageService.java @@ -0,0 +1,31 @@ +package com.sonic.frog.service; + + +/** + * 公共发送系统通知 + */ +public interface CommonMessageService { + + /** + * ai被送礼物发送系统通知 + * + * @param + */ + void aiGiftSendMessage(Long userId, Long aiId, String giftName, Integer giftNum, Long totalAmount); + + /** + * ai心跳等级降级发送系统通知 + */ + void aiHeartbeatLevelDowngradeSendMessage(Long userId, Long aiId, String heartbeatLevelName); + + /** + * 图片解锁发送系统通知 + * + * @param userId + * @param aiId + * @param albumId + * @param unlockAmount + */ + void unlockAlbumImgSendMessage(Long userId, Long aiId, Long albumId, Long unlockAmount); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/CommonSendMqService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/CommonSendMqService.java new file mode 100644 index 0000000..0392a5e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/CommonSendMqService.java @@ -0,0 +1,79 @@ +package com.sonic.frog.service; + + +import com.sonic.frog.enums.MessageTypeEnum; +import com.sonic.frog.event.inner.payload.AiImInfoPayload; +import com.sonic.frog.event.inner.payload.AiUserStatPayload; +import com.sonic.frog.event.inner.payload.CalcHeartbeatLevelPayload; +import com.sonic.frog.event.outer.payload.AiChatPayload; + +import java.math.BigDecimal; + +/** + * 发送消息到im的mq + */ +public interface CommonSendMqService { + + /** + * @param payload + */ + void aiImInfoSendMq(AiImInfoPayload payload); + + + /** + * 计算心动等级发送mq + * + * @param payload + */ + void calHeartbeatLevelSendMq(CalcHeartbeatLevelPayload payload); + + /** + * 发送AI聊天的MQ消息 + * + * @param fromUserId + * @param toUserId + * @param content + */ + void sendAiChatMq(Long fromUserId, Long toUserId, String content, MessageTypeEnum messageType, String attach, AiChatPayload.SourceType sourceType); + + /** + * 发送心动分计算MQ消息 + * + * @param fromUserId + * @param toUserId + */ + void calcHeartbeatScoreMq(Long fromUserId, Long toUserId); + + /** + * 发送心动分榜单计算MQ消息 + * + * @param aiId + * @param userId + * @param oldHeartbeatVal + * @param newHeartbeatVal + */ + void calcHeartbeatRankMq(Long aiId, Long userId, BigDecimal oldHeartbeatVal, BigDecimal newHeartbeatVal); + + /** + * 发送扣减心动值MQ消息 + * + * @param userId + */ + void subtractHeartbeatValMq(Long userId); + + /** + * Ai数据统计MQ消息 + * + * @param aiId + * @param type + * @param coinNum + */ + void aiUserStatMq(Long aiId, AiUserStatPayload.Type type, Long coinNum); + + /** + * ai创建,编辑,删除发送MQ消息 + * + * @param aiId + */ + void aiChangeMq(Long aiId, Boolean isDialoguePrologueChange, Boolean isCreate); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/ExploreService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/ExploreService.java new file mode 100644 index 0000000..162e801 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/ExploreService.java @@ -0,0 +1,14 @@ +package com.sonic.frog.service; + +import com.sonic.frog.domain.output.ExploreInfoOutput; + +public interface ExploreService { + + /** + * 探索信息 + * + * @param userId + * @return + */ + ExploreInfoOutput exploreInfo(Long userId); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/GiftDictService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/GiftDictService.java new file mode 100644 index 0000000..99ec18f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/GiftDictService.java @@ -0,0 +1,20 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.common.rpc.Page; +import com.sonic.frog.domain.entity.GiftDict; +import com.sonic.frog.domain.input.GiftDictListInput; +import com.sonic.frog.domain.output.GiftDictListOutput; + +/** + * 礼物字典业务接口 + */ +public interface GiftDictService extends IService { + /** + * 获取礼物字典列表 + * + * @param input + * @return + */ + Page getGiftDictList(GiftDictListInput input); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/GiftRewardRecordService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/GiftRewardRecordService.java new file mode 100644 index 0000000..246e950 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/GiftRewardRecordService.java @@ -0,0 +1,21 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.GiftDict; +import com.sonic.frog.domain.entity.GiftRewardRecord; + +/** + * 礼物打赏记录服务接口 + */ +public interface GiftRewardRecordService extends IService { + + /** + * 增加礼物打赏记录 + * @param giftDict + * @param orderNo + * @param fromUid + * @param toUid + * @param num + */ + void addGiftRewardRecord(GiftDict giftDict, String orderNo, Long fromUid, Long toUid, Integer num); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/HeartbeatLevelDictService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/HeartbeatLevelDictService.java new file mode 100644 index 0000000..35b9602 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/HeartbeatLevelDictService.java @@ -0,0 +1,58 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.HeartbeatLevelDict; +import com.sonic.frog.domain.output.HeartbeatLevelDictOutput; +import com.sonic.frog.enums.HeartbeatLevelEnum; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 心动等级字典表服务接口 + */ +public interface HeartbeatLevelDictService extends IService { + + /** + * 获取解锁的等级列表 + * + * @param code + * @return + */ + List getUnlockListByCode(HeartbeatLevelEnum code); + + /** + * 获取心动等级字典列表 + * + * @param aiUserHeartbeatLevel 当前用户与AI的心动等级 + * @return + */ + List getHearbeatLevelDictList(HeartbeatLevelEnum aiUserHeartbeatLevel); + + /** + * 根据心动值获取对应的心动等级 + * + * @param heartbeatVal + * @return + */ + HeartbeatLevelDict getHeartbeatLevelByVal(BigDecimal heartbeatVal); + + /** + * 根据心动等级枚举获取对应的心动等级 + * + * @param heartbeatLevelEnum + * @return + */ + HeartbeatLevelDict getHeartbeatLevelDictByLevel(HeartbeatLevelEnum heartbeatLevelEnum); + + /** + * 批量获取心动等级字典 + * + * @param heartbeatLevelEnums + * @return + */ + Map mapByLevel(Set heartbeatLevelEnums); + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/HomeClassificationService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/HomeClassificationService.java new file mode 100644 index 0000000..7e9bc67 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/HomeClassificationService.java @@ -0,0 +1,30 @@ +package com.sonic.frog.service; + +import com.sonic.frog.domain.input.ClassificationListInput; +import com.sonic.frog.domain.input.HomeRecommendInput; +import com.sonic.frog.domain.output.ClassificationListOutput; +import com.sonic.frog.domain.output.HomeRecommendOutput; + +import java.util.List; + +/** + * 首页分类推荐 + */ +public interface HomeClassificationService { + + /** + * 首页分类列表 + * + * @param userId + * @param input + * @return + */ + List classificationList(Long userId, ClassificationListInput input); + + + /** + * 缓存首页分类默认条件列表 + */ + void homeClassificationUpdateCache(ClassificationListInput input); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/HomeRecommendService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/HomeRecommendService.java new file mode 100644 index 0000000..c5b5347 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/HomeRecommendService.java @@ -0,0 +1,28 @@ +package com.sonic.frog.service; + +import com.sonic.frog.domain.input.HomeRecommendInput; +import com.sonic.frog.domain.output.HomeRecommendOutput; + +import java.util.List; + +/** + * 首页推荐 + */ +public interface HomeRecommendService { + + /** + * 首页推荐列表 + * @param currentUserId + * @param input + * @return + */ + List recommendList(Long currentUserId, HomeRecommendInput input); + + /** + * 获取AI的详情 + * @param currentUserId + * @param aiId + * @return + */ + HomeRecommendOutput getAiMeetDetail(Long currentUserId, Long aiId); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/HomeRecommendV2Service.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/HomeRecommendV2Service.java new file mode 100644 index 0000000..725ac35 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/HomeRecommendV2Service.java @@ -0,0 +1,23 @@ +package com.sonic.frog.service; + +import com.sonic.frog.domain.output.HomeRecommendV2Output; + +/** + * 首页推荐 + */ +public interface HomeRecommendV2Service { + + /** + * 更新缓存数据 + */ + void updateCache(); + + + /** + * 获取首页推荐列表 + * @return + */ + HomeRecommendV2Output recommendList(); + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/ImageStyleDictService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/ImageStyleDictService.java new file mode 100644 index 0000000..0e38fb8 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/ImageStyleDictService.java @@ -0,0 +1,33 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.ImageStyleDict; + +import java.util.List; + +/** + *

+ * 形象风格图片表 服务类 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +public interface ImageStyleDictService extends IService { + + /** + * 获取所有形象风格图片 + * + * @return + */ + List getAllImageStyleDictList(); + + /** + * 根据code获取形象风格 + * + * @param imageStyleCode + * @return + */ + ImageStyleDict getImageStyleDictByCode(String imageStyleCode); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/LikedService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/LikedService.java new file mode 100644 index 0000000..957dd30 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/LikedService.java @@ -0,0 +1,28 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.Liked; +import com.sonic.frog.domain.input.AiUserLikeOrCancelInput; + +/** + * 点赞业务接口 + */ +public interface LikedService extends IService { + /** + * ai用户点赞、取消点赞 + * + * @param userId + * @param input + */ + void aiUserLikeOrCancel(Long userId, AiUserLikeOrCancelInput input); + + /** + * 判断用户是否点赞过bizId + * + * @param userId + * @param bizId + * @param bizType + * @return + */ + Boolean isLiked(Long userId, Long bizId, Liked.BizType bizType); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/MeetService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/MeetService.java new file mode 100644 index 0000000..acbc9cf --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/MeetService.java @@ -0,0 +1,15 @@ +package com.sonic.frog.service; + +import com.sonic.frog.domain.input.MeetUnlockInput; + +public interface MeetService { + + + /** + * meet解锁 + * @param currentUserId + * @param input + */ + void meetUnLock(Long currentUserId, MeetUnlockInput input); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/MeetUnlockService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/MeetUnlockService.java new file mode 100644 index 0000000..4359244 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/MeetUnlockService.java @@ -0,0 +1,7 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.MeetUnlock; + +public interface MeetUnlockService extends IService { +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/MockService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/MockService.java new file mode 100644 index 0000000..29a803c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/MockService.java @@ -0,0 +1,14 @@ +package com.sonic.frog.service; + +import com.mashape.unirest.http.exceptions.UnirestException; + +public interface MockService { + + + /** + * 更新ai用户扩展信息 + * @param aiId + */ + void updateAiUserExt(Long aiId) throws UnirestException; + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/PayService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/PayService.java new file mode 100644 index 0000000..135b9bf --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/PayService.java @@ -0,0 +1,25 @@ +package com.sonic.frog.service; + +import com.sonic.frog.domain.input.BuyCreateImageCountInput; +import com.sonic.frog.domain.input.UnlockLikeYouInput; +import com.sonic.frog.domain.output.ViewUnlockAlbumImgOutput; + +import javax.validation.Valid; + +public interface PayService { + /** + * 购买创建图片次数 + * + * @param input + * @param userId + */ + void buyCreateImageCount(BuyCreateImageCountInput input, Long userId); + + /** + * 解锁爱慕者 + * + * @param input + * @param userId + */ + ViewUnlockAlbumImgOutput unlockLikeYou(UnlockLikeYouInput input, Long userId); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/RankService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/RankService.java new file mode 100644 index 0000000..dea5273 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/RankService.java @@ -0,0 +1,56 @@ +package com.sonic.frog.service; + +import com.sonic.frog.domain.output.AiChatRankOutput; +import com.sonic.frog.domain.output.AiGiftRankOutput; +import com.sonic.frog.domain.output.AiHeartbeatRankOutput; + +import java.util.List; + +public interface RankService { + + /** + * 聊天榜单定时任务计算 + * + * @return + */ + int chatRankJob(); + + /** + * 聊天榜单 + * + * @param userId + * @return + */ + List chatRank(Long userId, Integer limit); + + /** + * 心跳榜单定时任务计算 + * + * @return + */ + int aiHeartbeatRankJob(); + + /** + * 聊天榜单 + * + * @param userId + * @return + */ + List aiHeartbeatRank(Long userId, Integer limit); + + + /** + * 礼物榜单定时任务计算 + * + * @return + */ + int giftRankJob(); + + /** + * 礼物榜单 + * + * @param userId + * @return + */ + List giftRank(Long userId, Integer limit); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/SignInRecordService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/SignInRecordService.java new file mode 100644 index 0000000..c959024 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/SignInRecordService.java @@ -0,0 +1,26 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.SignInRecord; +import com.sonic.frog.domain.entity.SignInStat; + +/** + * 签到明细表 + * @author zzhan + */ +public interface SignInRecordService extends IService { + + /** + * 签到处理 + * @param currentUserId + */ + Boolean signIn(Long currentUserId); + + /** + * 签到数据初始化 + * @param currentUserId + * @return + */ + SignInStat init(Long currentUserId); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/SignInStatSearchService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/SignInStatSearchService.java new file mode 100644 index 0000000..72ab06d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/SignInStatSearchService.java @@ -0,0 +1,19 @@ +package com.sonic.frog.service; + + +import com.sonic.frog.domain.output.SignInRoundOutput; + +/** + * 签到统计数据表查询 + * @author zzhan + */ +public interface SignInStatSearchService { + + /** + * 获取当前登录用户的七天签到数据 + * @param currentUserId + * @return + */ + SignInRoundOutput signInList(Long currentUserId); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/SignInStatService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/SignInStatService.java new file mode 100644 index 0000000..2c8ef30 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/SignInStatService.java @@ -0,0 +1,41 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.SignInStat; + +/** + * 签到统计数据表 + * @author zzhan + */ +public interface SignInStatService extends IService { + + /** + * 获取签到统计基础数据 + * @param currentUserId + * @return + */ + SignInStat get(Long currentUserId); + + /** + * 初始化一条数据 + * @param currentUserId + * @return + */ + SignInStat init(Long currentUserId); + + /** + * 更新起止天 + * @param id + * @param startDay + * @param endDay + */ + void updateStartEndDayAndDays(Long id, String startDay, String endDay); + + /** + * 更新月统计数据 + * @param id + * @param days + */ + void updateStat(Long id, Integer days); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/ThirdLoginOrRegisterService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/ThirdLoginOrRegisterService.java new file mode 100644 index 0000000..21a0277 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/ThirdLoginOrRegisterService.java @@ -0,0 +1,21 @@ +package com.sonic.frog.service; + + +import com.sonic.frog.domain.input.ThirdLoginOrRegisterInput; +import com.sonic.frog.domain.output.ThirdLoginOrRegisterOutput; + +/** + * @Author code + * @Description 登录或注册验证 + * @Version 1.0 + */ +public interface ThirdLoginOrRegisterService { + + /** + * 登录或注册验证 + * @param input + * @return + */ + ThirdLoginOrRegisterOutput loginOrRegister(ThirdLoginOrRegisterInput input); + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/TimbreDictService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/TimbreDictService.java new file mode 100644 index 0000000..bde312f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/TimbreDictService.java @@ -0,0 +1,27 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.TimbreDict; + +import java.util.List; + +/** + * 音色字典表服务接口 + */ +public interface TimbreDictService extends IService { + + /** + * 获取所有音色推荐列表 + * + * @return + */ + List getAllTimbreDictList(); + + /** + * 根据code获取音色字典 + * + * @param code + * @return + */ + TimbreDict getTimbreDictByCode(String code); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/UserAiMeetService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/UserAiMeetService.java new file mode 100644 index 0000000..d4477ac --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/UserAiMeetService.java @@ -0,0 +1,49 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.UserAiMeet; +import com.sonic.frog.domain.output.MeetSdOutput; + +/** + *

+ * 用户和AI相互喜欢记录表 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +public interface UserAiMeetService extends IService { + + /** + * 滑动上报 + * @param userId + * @param aiId + * @param lk + * @return + */ + MeetSdOutput meetSd(Long userId, Long aiId, boolean lk); + + /** + * 检查并添加喜欢记录 + * @param userId + * @param aiId + * @return + */ + boolean meetBd(Long userId, Long aiId); + + /** + * meet爱慕者推荐 + * @param userId + * @return + */ + Long meetRc(Long userId); + + /** + * 添加喜欢记录 + * @param userId + * @param aiId + */ + boolean addMeet(Long userId, Long aiId); + +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/UserCreateCountStatService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/UserCreateCountStatService.java new file mode 100644 index 0000000..b93d47d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/UserCreateCountStatService.java @@ -0,0 +1,51 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.UserCreateCountStat; +import com.sonic.frog.domain.output.UserCreateCountOutput; + +import java.time.LocalDateTime; + +/** + * 用户相册创作次数统计 Service + */ +public interface UserCreateCountStatService extends IService { + /** + * 购买时,增加用户创作购买次数 + * + * @param userId + * @param count + */ + void addBuyNum(Long userId, Integer count); + + + /** + * 订阅会员或续订时,增加用户会员创作次数 + * + * @param userId + * @param startTime 订阅会员时间或续订会员时间 + * @param expireTime 会员过期时间 + */ + void subMemberGiftUserCreateCount(Long userId, LocalDateTime startTime, LocalDateTime expireTime); + + + /** + * 获取用户创作次数 + * + * @param userId + * @return + */ + UserCreateCountOutput getUserCreateCount(Long userId); + + /** + * 使用用户创作次数 + * + * @param userId + */ + void useUserCreateCount(Long userId); + + /** + * 会员每隔一个月赠送会员次数 + */ + void giftMemberNumJob(); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/UserDeductionStatService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/UserDeductionStatService.java new file mode 100644 index 0000000..ae7472d --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/UserDeductionStatService.java @@ -0,0 +1,40 @@ +package com.sonic.frog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.frog.domain.entity.UserDeductionStat; +import com.sonic.frog.enums.DeductionTypeEnum; + +/** + * 用户聊天、语音,语音通话扣费统计表 Service + */ +public interface UserDeductionStatService extends IService { + + /** + * 发送文本聊天,语音聊天,语音通话扣费统计 + * + * @param userId + * @param aiId + * @param deductionType + */ + void userDeductionStat(Long userId, Long aiId, DeductionTypeEnum deductionType, String extra); + + /** + * 定时任务,文本聊天,语音聊天超过10分钟未聊天扣费 + */ + Integer textOrVoiceTypeNoChatCheckoutJob(); + + /** + * 获取用户总预扣费金额 + * + * @param userId + * @return + */ + Long getTotalDeductionAmount(Long userId); + + /** + * 余额不足时,发起结算,生成流水 + * + * @param userId + */ + void userDeductionCheckout(Long userId); +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/UserService.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/UserService.java new file mode 100644 index 0000000..98df8bf --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/UserService.java @@ -0,0 +1,46 @@ +package com.sonic.frog.service; + +import com.sonic.bear.lib.input.CompleteUserInfoInput; +import com.sonic.bear.lib.input.EditUserInfoInput; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.frog.domain.output.UserBaseInfoOutput; + +public interface UserService { + + /** + * 获取用户自己的基础信息(内部用) + * + * @param userId + * @return + */ + BaseUserInfoOutput baseUserInfo(Long userId); + + /** + * 获取用户自己的基础信息(接口用) + * + * @param userId + * @return + */ + UserBaseInfoOutput getbaseUserInfo(Long userId); + + /** + * 完善用户基础信息 + * + * @param input + */ + void completeUserInfo(CompleteUserInfoInput input); + + /** + * 编辑用户基础信息 + * + * @param input + */ + void editUserInfo(EditUserInfoInput input); + + /** + * 删除账号 + * + * @param userId + */ + void delAccount(Long userId); +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AdvertiseServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AdvertiseServiceImpl.java new file mode 100644 index 0000000..59c750b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AdvertiseServiceImpl.java @@ -0,0 +1,86 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.AdvertiseDao; +import com.sonic.frog.domain.entity.Advertise; +import com.sonic.frog.domain.entity.AiDict; +import com.sonic.frog.domain.output.AdvertiseOutput; +import com.sonic.frog.domain.output.AiCarouselListOutput; +import com.sonic.frog.domain.output.AiUserBaseOutput; +import com.sonic.frog.domain.output.ClassificationListOutput; +import com.sonic.frog.service.AdvertiseService; +import com.sonic.frog.service.AiUserSearchService; +import com.sonic.frog.service.AiUserStatService; +import com.sonic.frog.utils.BeanConver; +import com.sonic.frog.utils.CacheUtils; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.sonic.frog.enums.AdvertiseBizType; + +/** + * 广告管理 Service实现类 + */ +@Slf4j +@Service +public class AdvertiseServiceImpl extends ServiceImpl implements AdvertiseService { + + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private AiUserStatService aiUserStatService; + @Autowired + private CacheUtils cacheUtils; + @Autowired + private RedisKeyUtils redisKeyUtils; + + + @Override + public List getAdvertiseList(Long userId) { + List list = list(Wrappers.lambdaQuery().eq(Advertise::getIsDelete, false).orderByAsc(Advertise::getSort)); + return BeanConver.copeList(list, AdvertiseOutput.class); + } + + @Override + public List getAiCarouselList(Long userId) { + //获取ai轮播推荐数据 + Advertise advertise = getOne(Wrappers.lambdaQuery().eq(Advertise::getBizType, AdvertiseBizType.AI_CAROUSEL_RECOMMEND).eq(Advertise::getIsDelete, false).last("limit 1")); + if (advertise != null) { + String ext = advertise.getExt(); + List aiIdList = JSON.parseArray(ext, Long.class); + if (CollectionUtils.isEmpty(aiIdList)) { + return Collections.emptyList(); + } + //缓存1分钟 + String aiCarouselListKey = redisKeyUtils.aiCarouselListKey(); + return cacheUtils.getCacheListAndSet(aiCarouselListKey, AiCarouselListOutput.class, () -> { + //批量获取ai基础信息Map + Map aiUserMap = aiUserSearchService.mapByAiIdList(aiIdList); + //批量获取被喜欢数Map + Map aiLikedCountMap = aiUserStatService.queryAiLikedCount(aiIdList); + List outputList = aiIdList.stream().map(aiId -> { + AiUserBaseOutput aiUserBaseOutput = aiUserMap.get(aiId); + //基础信息赋值 + AiCarouselListOutput output = BeanConver.copeBean(aiUserBaseOutput, AiCarouselListOutput.class); + if (output != null) { + //被喜欢数赋值 + output.setLikedCount(aiLikedCountMap.get(aiId) != null ? aiLikedCountMap.get(aiId) : 0); + } + return output; + }).collect(Collectors.toList()); + return JSON.toJSONString(outputList); + }, 60); + } + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiChatInfoServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiChatInfoServiceImpl.java new file mode 100644 index 0000000..a345cd5 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiChatInfoServiceImpl.java @@ -0,0 +1,70 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Lists; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.frog.domain.entity.AiUserHeartbeatRelation; +import com.sonic.frog.domain.entity.ChatSet; +import com.sonic.frog.domain.entity.Liked; +import com.sonic.frog.domain.entity.UserAiMeet; +import com.sonic.frog.domain.output.AiChatInfoOutput; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import com.sonic.frog.service.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class AiChatInfoServiceImpl implements AiChatInfoService { + + @Autowired + private ChatSetService chatSetService; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private UserService userService; + @Autowired + private LikedService likedService; + @Autowired + private HeartbeatLevelDictService heartbeatLevelDictService; + + @Override + public AiChatInfoOutput getAiChatInfo(Long userId, Long aiId) { + AiChatInfoOutput aiChatInfoOutput = new AiChatInfoOutput(); + //获取当前用户基础信息 + BaseUserInfoOutput baseUserInfo = userService.baseUserInfo(userId); + //昵称,性别,生日初始用户基础信息 + aiChatInfoOutput.setNickname(baseUserInfo.getNickname()); + aiChatInfoOutput.setSex(baseUserInfo.getSex()); + aiChatInfoOutput.setBirthday(baseUserInfo.getBirthday()); + //查询基础 信息 + ChatSet chatSet = chatSetService.getOne(Wrappers.lambdaQuery().eq(ChatSet::getUserId, userId).eq(ChatSet::getAiId, aiId)); + if (chatSet != null) { + aiChatInfoOutput.setNickname(StringUtils.isNotEmpty(chatSet.getNickname()) ? chatSet.getNickname() : baseUserInfo.getNickname()); + aiChatInfoOutput.setSex(chatSet.getSex()); + aiChatInfoOutput.setBirthday(chatSet.getBirthday() != null ? chatSet.getBirthday() : baseUserInfo.getBirthday()); + aiChatInfoOutput.setWhoAmI(StringUtils.isNotEmpty(chatSet.getWhoAmI()) ? chatSet.getWhoAmI() : ""); + } + //查询相识天数 + AiUserHeartbeatRelation aiUserHeartbeatRelation = aiUserHeartbeatRelationService.getOne(Wrappers.lambdaQuery().eq(AiUserHeartbeatRelation::getUserId, userId).eq(AiUserHeartbeatRelation::getAiId, aiId)); + //解锁了哪些心动等级 + aiChatInfoOutput.setUnlockHearbeatLevelList(Lists.newArrayList()); + if (aiUserHeartbeatRelation != null) { + aiChatInfoOutput.setDayCount(aiUserHeartbeatRelation.getDayCount()); + aiChatInfoOutput.setHeartbeatLevel(aiUserHeartbeatRelation.getHeartbeatLevel() == null ? HeartbeatLevelEnum.LEVEL_1 : aiUserHeartbeatRelation.getHeartbeatLevel()); + aiChatInfoOutput.setRelationStage(aiUserHeartbeatRelation.getHeartbeatLevel() == null ? HeartbeatLevelEnum.LEVEL_1.getRelationStage() : aiUserHeartbeatRelation.getHeartbeatLevel().getRelationStage()); + aiChatInfoOutput.setUnlockHearbeatLevelList(heartbeatLevelDictService.getUnlockListByCode(aiChatInfoOutput.getHeartbeatLevel())); + } else { + aiChatInfoOutput.setDayCount(0); + aiChatInfoOutput.setHeartbeatLevel(HeartbeatLevelEnum.LEVEL_1); + aiChatInfoOutput.setRelationStage(HeartbeatLevelEnum.LEVEL_1.getRelationStage()); + } + //查询左滑又滑相识 + int count = likedService.count(Wrappers.lambdaQuery().eq(Liked::getLikedUserId, userId).eq(Liked::getAiId, aiId)); + aiChatInfoOutput.setMeet(count > 0); + return aiChatInfoOutput; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiDictServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiDictServiceImpl.java new file mode 100644 index 0000000..8006aef --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiDictServiceImpl.java @@ -0,0 +1,114 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Maps; +import com.sonic.frog.dao.AiDictDao; +import com.sonic.frog.domain.entity.AiDict; +import com.sonic.frog.domain.entity.ImageStyleDict; +import com.sonic.frog.domain.output.AiDictOut; +import com.sonic.frog.domain.output.DictOutput; +import com.sonic.frog.enums.AiDictTypeEnum; +import com.sonic.frog.service.AiDictService; +import com.sonic.frog.service.ImageStyleDictService; +import com.sonic.frog.service.TimbreDictService; +import com.sonic.frog.utils.BeanConver; +import com.sonic.frog.utils.CacheUtils; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + *

+ * AI角色,性格,标签,形象风格字典表 服务实现类 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +@Service +@Slf4j +public class AiDictServiceImpl extends ServiceImpl implements AiDictService { + + @Autowired + private CacheUtils cacheUtils; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private ImageStyleDictService imageStyleDictService; + @Autowired + private TimbreDictService timbreDictService; + + @Override + public AiDictOut getAiDict() { + String aiDictCacheKey = redisKeyUtils.aiDictCacheKey(); + List aiDictCacheList = cacheUtils.getCacheListAndSet(aiDictCacheKey, AiDict.class, () -> { + List aiDictList = list(Wrappers.lambdaQuery().eq(AiDict::getIsDelete, false)); + return JSON.toJSONString(aiDictList); + }, 60 * 60 * 1); + AiDictOut aiDictOut = new AiDictOut(); + if (CollectionUtils.isNotEmpty(aiDictCacheList)) { + //角色字典获取 + List roleDictOutputList = buildDictOutputList(AiDictTypeEnum.ROLE, aiDictCacheList); + roleDictOutputList.forEach(e -> { + //获取子级角色字典 + List childRoleDictList = aiDictCacheList.stream().filter(child -> child.getParentCode() != null && child.getParentCode().equals(e.getCode())).collect(Collectors.toList()); + e.setChildDictList(BeanConver.copeList(childRoleDictList, DictOutput.class)); + }); + //性格字典获取 + List characterDictOutputList = buildDictOutputList(AiDictTypeEnum.CHARACTER, aiDictCacheList); + characterDictOutputList.forEach(e -> { + //获取子级性格字典,对应的标签 + List childCharacterDictList = aiDictCacheList.stream().filter(child -> child.getParentCode() != null && child.getParentCode().equals(e.getCode())).collect(Collectors.toList()); + e.setChildDictList(BeanConver.copeList(childCharacterDictList, DictOutput.class)); + }); + //标签字典获取 + List tagDictOutputList = buildDictOutputList(AiDictTypeEnum.TAG, aiDictCacheList); + //获取形象风格图片 + List allImageStyleDictList = imageStyleDictService.getAllImageStyleDictList(); + + //出参 + aiDictOut.setRoleDictList(roleDictOutputList); + aiDictOut.setCharacterDictList(characterDictOutputList); + aiDictOut.setTagDictList(tagDictOutputList); + aiDictOut.setImageStyleDictList(allImageStyleDictList); + } + //音色推荐 + aiDictOut.setTimbreDictList(timbreDictService.getAllTimbreDictList()); + return aiDictOut; + } + + /** + * 获取各类型字典列表 + * + * @param type + * @param aiDictCacheList + * @return + */ + private List buildDictOutputList(AiDictTypeEnum type, List aiDictCacheList) { + List aiDictList = aiDictCacheList.stream().filter(e -> type.equals(e.getType()) && StringUtils.isEmpty(e.getParentCode())).collect(Collectors.toList()); + return BeanConver.copeList(aiDictList, DictOutput.class); + } + + @Override + public Map mapNameByCodeList(Set codeList) { + if (CollectionUtils.isEmpty(codeList)) { + return Maps.newHashMap(); + } + List list = list(Wrappers.lambdaQuery().eq(AiDict::getIsDelete, false).in(AiDict::getCode, codeList)); + if (CollectionUtils.isNotEmpty(list)) { + return list.stream().collect(Collectors.toMap(AiDict::getCode, AiDict::getName)); + } + return Maps.newHashMap(); + } +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserAlbumServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserAlbumServiceImpl.java new file mode 100644 index 0000000..5f8dcfe --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserAlbumServiceImpl.java @@ -0,0 +1,621 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.sonic.common.rpc.Page; +import com.sonic.common.utils.CloudFrontSignerUtils; +import com.sonic.cow.lib.client.ImageClient; +import com.sonic.daosupport.converter.PageConverter; +import com.sonic.frog.dao.AiUserAlbumDao; +import com.sonic.frog.dao.LikedDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.AiUserAlbum; +import com.sonic.frog.domain.entity.Liked; +import com.sonic.frog.domain.input.*; +import com.sonic.frog.domain.output.AiAlbumDetailOutput; +import com.sonic.frog.domain.output.ListAiAlbumOutput; +import com.sonic.frog.domain.output.ViewUnlockAlbumImgOutput; +import com.sonic.frog.enums.LockStatusEnum; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.event.inner.payload.AiUserStatPayload; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.BeanConver; +import com.sonic.frog.utils.KeyGenerator; +import com.sonic.frog.utils.RegexUtil; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.enums.BizType; +import com.sonic.lion.lib.input.BalanceCheckoutInput; +import com.sonic.pigeon.lib.bo.AttachBo; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.input.UpdateAiSendCustomImageMessageInput; +import com.sonic.frog.lib.output.AIUserAlbumApiOutput; +import com.sonic.shark.lib.client.S3BlurryImgClient; +import com.sonic.shark.lib.client.S3CheckClient; +import com.sonic.shark.lib.enums.BlurryImgBizTypeEnum; +import com.sonic.shark.lib.input.CopyAndBlurryImgInput; +import com.sonic.shark.lib.output.BlurryRecordOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.security.InvalidKeyException; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +import static com.sonic.frog.enums.Constants.PLATFORM_RATE; + +/** + * AI用户相册业务实现类 + */ +@Service +@Slf4j +public class AiUserAlbumServiceImpl extends ServiceImpl implements AiUserAlbumService { + + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private LikedDao likedDao; + @Autowired + private AiUserAlbumDao aiUserAlbumDao; + @Autowired + private LikedService likedService; + @Autowired + private S3CheckClient s3CheckClient; + @Autowired + private S3BlurryImgClient copyAndBlurryImgClient; + @Autowired + private CloudFrontSignerUtils cloudFrontSignerUtils; + @Autowired + private AiUserAlbumUnlockService aiUserAlbumUnlockService; + @Autowired + private AiUserSetService aiUserSetService; + @Autowired + private CommonMessageService commonMessageService; + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private UserAiMeetService userAiMeetService; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private PayClient payClient; + @Autowired + private ImageClient imageClient; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + + @Override + public List batchAddAlbum(Long currentUserId, BatchAddAlbumInput addAlbum) { + List images = addAlbum.getImages().stream().filter(info -> RegexUtil.isOssStartUrl(info.getUrl())).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(images)) { + return null; + } + //最多添加一张 + ToastResultCode.ALBUM_ADD_MAX_ONE.check(images.size() > 1); + //判断添加的图片是否是ai生成的6张图 + imageClient.checkImageIsAIGenerated(currentUserId, images.stream().map(AiAlbumImageInfo::getUrl).collect(Collectors.toList())); + //权限检验,只有主人才能修改 + AiUser aiUser = aiUserSearchService.getAiUserByAiId(addAlbum.getAiId()); + ToastResultCode.SYS_PERMISSION_DENIED.check(aiUser == null || (aiUser.getUserId() != null && !aiUser.getUserId().equals(currentUserId))); + //TODO 校验提交的图片地址是不是符合规范的,是否已经鉴黄通过 +// s3CheckClient.checkImage(images.stream().map(AiAlbumImageInfo::getUrl).collect(Collectors.toList())); + List idList = Lists.newArrayList(); + //相册模糊处理 + List copyAndBlurryImgInputList = Lists.newArrayList(); + for (AiAlbumImageInfo imageInfo : images) { + AiUserAlbum aiUserAlbum = AiUserAlbum.builder() + .imgUrl(imageInfo.getUrl()) + .sourceImgUrl(imageInfo.getUrl()) + .width(imageInfo.getWidth()) + .height(imageInfo.getHeight()) + .aiId(addAlbum.getAiId()) + .userId(currentUserId) + .isDelete(false) + .isDefault(false) + .unlockPrice(imageInfo.getUnlockPrice()) + .createTime(LocalDateTime.now()) + .editTime(LocalDateTime.now()) + .build(); + save(aiUserAlbum); + idList.add(aiUserAlbum.getId()); + + //解锁价格大于0 需要模糊处理 + if (imageInfo.getUnlockPrice() > 0) { + CopyAndBlurryImgInput copyAndBlurryImgInput = new CopyAndBlurryImgInput(); + copyAndBlurryImgInput.setSourceFileUrl(imageInfo.getUrl()); + copyAndBlurryImgInput.setUserId(currentUserId); + copyAndBlurryImgInput.setBizType(BlurryImgBizTypeEnum.ALBUM); + copyAndBlurryImgInput.setBizId(aiUserAlbum.getId()); + copyAndBlurryImgInputList.add(copyAndBlurryImgInput); + } + } + if (CollectionUtils.isNotEmpty(copyAndBlurryImgInputList)) { + //调用oss服务内部api模糊处理 + Map blurryAlbumImgMap = copyAndBlurryImgClient.copyAndBlurryAlbumImg(copyAndBlurryImgInputList); + List updateUserAlbumList = Lists.newArrayList(); + for (CopyAndBlurryImgInput copyAndBlurryImgInput : copyAndBlurryImgInputList) { + Long bizId = copyAndBlurryImgInput.getBizId(); + AiUserAlbum userAlbum = new AiUserAlbum(); + userAlbum.setId(bizId); + userAlbum.setImgUrl(blurryAlbumImgMap.get(bizId)); + updateUserAlbumList.add(userAlbum); + } + //批量更新 + if (CollectionUtils.isNotEmpty(updateUserAlbumList)) { + updateBatchById(updateUserAlbumList); + } + } + return idList; + } + + @Override + public void setDefaultAlbum(Long currentUserId, Long aiId, Long albumId) { + //权限检验,只有主人才能修改 + AiUserAlbum aiUserAlbum = getOne(Wrappers.lambdaQuery().eq(AiUserAlbum::getId, albumId)); + ToastResultCode.SYS_PERMISSION_DENIED.check(aiUserAlbum == null); + ToastResultCode.SYS_PERMISSION_DENIED.check(!aiUserAlbum.getUserId().equals(currentUserId)); + //之前的默认图变成非默认图 + update(Wrappers.lambdaUpdate().eq(AiUserAlbum::getAiId, aiUserAlbum.getAiId()) + .eq(AiUserAlbum::getIsDefault, true) + .set(AiUserAlbum::getIsDefault, false)); + //设置为默认图片 + update(Wrappers.lambdaUpdate() + .eq(AiUserAlbum::getId, albumId) + .set(AiUserAlbum::getIsDefault, true) + .set(AiUserAlbum::getImgUrl, aiUserAlbum.getSourceImgUrl()) + .set(AiUserAlbum::getUnlockPrice, 0)); + //该图片替换为虚拟角色的个人主页头图,卡片主图,聊天背景(但不影响用户自定义设置的聊天背景)-需求变更-7.9 + aiUserSetService.setHomeImageUrl(aiId, aiUserAlbum.getSourceImgUrl()); + } + + @Override + public void delAlbum(Long currentUserId, Long albumId) { + //权限检验,只有主人才能修改 + AiUserAlbum aiUserAlbum = getOne(Wrappers.lambdaQuery().eq(AiUserAlbum::getId, albumId)); + ToastResultCode.ALBUM_NOT_EXIST.check(aiUserAlbum == null); + ToastResultCode.SYS_PERMISSION_DENIED.check(!aiUserAlbum.getUserId().equals(currentUserId)); + ToastResultCode.ALBUM_IS_DELETED.check(aiUserAlbum.getIsDelete()); + //不能删除默认图片 不可删除封面默认图片,该图片会作为在个人主页头图,卡片主图,聊天背景 + ToastResultCode.ALBUM_DEFAULT_CAN_NOT_DELETE.check(aiUserAlbum.getIsDefault()); + //设置为为删除 + update(Wrappers.lambdaUpdate().eq(AiUserAlbum::getId, albumId).set(AiUserAlbum::getIsDelete, true)); + } + + @Override + public void delByAiId(Long aiId) { + //删除ai相册所有图片 + update(Wrappers.lambdaUpdate().eq(AiUserAlbum::getAiId, aiId).set(AiUserAlbum::getIsDelete, true)); + } + + @Override + public void likeOrCancelPic(Long currentUserId, LikeOrCancelPicInput input) { + //获取相册信息 + AiUserAlbum userAlbum = getById(input.getAlbumId()); + ToastResultCode.ALBUM_NOT_EXIST.check(userAlbum == null); + //校验用户是否拥有操作权限 + aiUserAlbumUnlockService.checkAlbumLockStatus(currentUserId, userAlbum); + //加密图片不能点赞或取消点赞 + Boolean isUnlockAlbumId = aiUserAlbumUnlockService.isUnlockAlbumId(currentUserId, input.getAlbumId()); + ToastResultCode.ALBUM_ENCRYPT_CAN_NOT_LIKE.check(!isUnlockAlbumId && userAlbum.getUnlockPrice() != null && userAlbum.getUnlockPrice() > 0); + //查询点赞信息 + Liked liked = likedDao.selectOne(Wrappers.lambdaQuery() + .eq(Liked::getBizId, input.getAlbumId()) + .eq(Liked::getBizType, Liked.BizType.ALBUM_PIC) + .eq(Liked::getLikedUserId, currentUserId)); + //如果数据库状态与传过来的状态一致,不处理 + if (liked != null && liked.getLikedStatus() == input.getLikedStatus()) { + return; + } + //点赞或取消点赞 + likedDao.likeOrCancel(Liked.builder() + .bizId(input.getAlbumId()) + .bizType(Liked.BizType.ALBUM_PIC) + .aiId(userAlbum.getAiId()) + .likedUserId(currentUserId) + .likedStatus(input.getLikedStatus()).build()); + //点赞的话,点赞数加1,取消点赞减1 + aiUserAlbumDao.incrementLikedCount(input.getAlbumId(), input.getLikedStatus() == Liked.LikedStatus.LIKED ? 1 : -1); + } + + @Override + public Page listAlbums(AlbumListInput input, Long currentUserId) { + //当前用户解锁过的相册id列表 + List unlockAlbumIds = aiUserAlbumUnlockService.getUnlockAlbumIds(currentUserId); + //分页查询 + IPage albumPage = aiUserAlbumDao.listAlbumsPage(new com.baomidou.mybatisplus.extension.plugins.pagination.Page(input.getPage().getPn(), input.getPage().getPs()), input.getAiId(), unlockAlbumIds); + List aiUserAlbumList = albumPage != null ? albumPage.getRecords() : Lists.newArrayList(); + //没有数据,直接返回 + if (CollectionUtils.isEmpty(aiUserAlbumList)) { + return new Page<>(); + } + //当前登录用户对各相册点赞状态Map + Map albumIdLikedStatusMap = ImmutableMap.of(); + if (currentUserId != null) { + //业务主键id + List bizIds = aiUserAlbumList.stream().map(AiUserAlbum::getId).collect(Collectors.toList()); + albumIdLikedStatusMap = likedService.list(Wrappers.lambdaQuery() + .in(Liked::getBizId, bizIds) + .eq(Liked::getBizType, Liked.BizType.ALBUM_PIC) + .eq(Liked::getLikedUserId, currentUserId) + ).stream().collect(Collectors.toMap(Liked::getBizId, Liked::getLikedStatus)); + } + Map finalAlbumIdLikedStatusMap = albumIdLikedStatusMap; + //内部调用oss服务api,获取图片的模糊图片 + List blurBizIds = aiUserAlbumList.stream().filter(aiUserAlbum -> aiUserAlbum.getUnlockPrice() > 0).map(AiUserAlbum::getId).collect(Collectors.toList()); + //相册模糊Map + Map blurryRecordOutputMap = new HashMap<>(16); + //相册锁状态Map + Map aiUserAlbumLockStatusMap = new HashMap<>(16); + if (CollectionUtils.isNotEmpty(blurBizIds)) { + //查询相册是否对访问用户解锁 + aiUserAlbumLockStatusMap = aiUserAlbumUnlockService.queryAlbumUnlock(currentUserId, aiUserAlbumList); + //批量查询相册模糊图片的基础信息 + blurryRecordOutputMap = copyAndBlurryImgClient.blurryRecordBizIdMap(BlurryImgBizTypeEnum.ALBUM.name(), blurBizIds); + } + Map finalBlurryRecordOutputMap = blurryRecordOutputMap; + Map finalAiUserAlbumLockStatusMap = aiUserAlbumLockStatusMap; + return PageConverter.convert(albumPage, aiUserAlbum -> { + ListAiAlbumOutput albumOutput = BeanConver.copeBean(aiUserAlbum, ListAiAlbumOutput.class); + albumOutput.setLikedStatus(finalAlbumIdLikedStatusMap.getOrDefault(aiUserAlbum.getId(), Liked.LikedStatus.CANCELED)); + albumOutput.setAlbumId(aiUserAlbum.getId()); + //私密图片 自己访问自己,锁状态一定为上锁,显示原图,如果是其他人访问,则需要看当前用户是否解锁该图片,如果付费解锁过,则解锁,显示原图,否则为上锁,显示模糊图片 + if (aiUserAlbum.getUnlockPrice() > 0) { + blurImgHandle(aiUserAlbum, albumOutput, finalBlurryRecordOutputMap, finalAiUserAlbumLockStatusMap, input.getIpAddress()); + } + return albumOutput; + }); + } + + @Override + public List listByIds(Long currentUserId, List idList) { + List aiUserAlbumList = (List) listByIds(idList); + //没有数据,直接返回 + if (CollectionUtils.isEmpty(aiUserAlbumList)) { + return Lists.newArrayList(); + } + //当前登录用户对各相册点赞状态Map + Map albumIdLikedStatusMap = ImmutableMap.of(); + if (currentUserId != null) { + //业务主键id + List bizIds = aiUserAlbumList.stream().map(AiUserAlbum::getId).collect(Collectors.toList()); + albumIdLikedStatusMap = likedService.list(Wrappers.lambdaQuery() + .in(Liked::getBizId, bizIds) + .eq(Liked::getBizType, Liked.BizType.ALBUM_PIC) + .eq(Liked::getLikedUserId, currentUserId) + ).stream().collect(Collectors.toMap(Liked::getBizId, Liked::getLikedStatus)); + } + Map finalAlbumIdLikedStatusMap = albumIdLikedStatusMap; + //内部调用oss服务api,获取图片的模糊图片 + List blurBizIds = aiUserAlbumList.stream().filter(aiUserAlbum -> aiUserAlbum.getUnlockPrice() > 0).map(AiUserAlbum::getId).collect(Collectors.toList()); + //相册模糊Map + Map blurryRecordOutputMap = new HashMap<>(16); + //相册锁状态Map + Map aiUserAlbumLockStatusMap = new HashMap<>(16); + if (CollectionUtils.isNotEmpty(blurBizIds)) { + //查询相册是否对访问用户解锁 + aiUserAlbumLockStatusMap = aiUserAlbumUnlockService.queryAlbumUnlock(currentUserId, aiUserAlbumList); + //批量查询相册模糊图片的基础信息 + blurryRecordOutputMap = copyAndBlurryImgClient.blurryRecordBizIdMap(BlurryImgBizTypeEnum.ALBUM.name(), blurBizIds); + } + Map finalBlurryRecordOutputMap = blurryRecordOutputMap; + Map finalAiUserAlbumLockStatusMap = aiUserAlbumLockStatusMap; + + List outputList = Lists.newArrayList(); + for (AiUserAlbum aiUserAlbum : aiUserAlbumList) { + ListAiAlbumOutput albumOutput = BeanConver.copeBean(aiUserAlbum, ListAiAlbumOutput.class); + albumOutput.setLikedStatus(finalAlbumIdLikedStatusMap.getOrDefault(aiUserAlbum.getId(), Liked.LikedStatus.CANCELED)); + albumOutput.setAlbumId(aiUserAlbum.getId()); + //私密图片 自己访问自己,锁状态一定为上锁,显示原图,如果是其他人访问,则需要看当前用户是否解锁该图片,如果付费解锁过,则解锁,显示原图,否则为上锁,显示模糊图片 + if (aiUserAlbum.getUnlockPrice() > 0) { + blurImgHandle(aiUserAlbum, albumOutput, finalBlurryRecordOutputMap, finalAiUserAlbumLockStatusMap, null); + } + outputList.add(albumOutput); + } + return outputList; + } + + public AiAlbumDetailOutput rcDetail(Long albumId) throws InvalidKeyException { + //获取相册图片 + AiUserAlbum album = getById(albumId); + //没有数据,直接返回 + if (album == null) { + return null; + } + //构造出参对象 + AiAlbumDetailOutput output = BeanConver.copeBean(album, AiAlbumDetailOutput.class); + output.setAiId(album.getAiId()); + output.setAlbumId(albumId); + //设置出参 + Long expTime = System.currentTimeMillis() + 3650 * 24 * 60 * 60 * 1000L; + output.setImg1(cloudFrontSignerUtils.signer(album.getImgUrl() + CloudFrontSignerUtils.BLURRY_IMG_468_600, new Date(expTime), null)); + output.setImg2(cloudFrontSignerUtils.signer(album.getImgUrl() + CloudFrontSignerUtils.BLURRY_IMG_800_800, new Date(expTime), null)); + output.setImg3(cloudFrontSignerUtils.signer(album.getImgUrl() + CloudFrontSignerUtils.BLURRY_ORG_IMG, new Date(expTime), null)); + return output; + } + + /** + * 模糊图片处理 + * 私密图片 自己访问自己,锁状态一定为上锁,显示原图,如果是其他人访问,则需要看当前用户是否订阅过该用户,如果订阅,则解锁,显示原图,否则为上锁,显示模糊图片 + * + * @param userAlbum + * @param albumOutput + * @param blurryRecordOutputMap + * @param aiUserAlbumLockStatusMap + * @param ipAddress + */ + private void blurImgHandle(AiUserAlbum userAlbum, ListAiAlbumOutput albumOutput, Map blurryRecordOutputMap, Map aiUserAlbumLockStatusMap, String ipAddress) { + try { + albumOutput.setImgUrl(null); + //设置图片的解锁状态 + albumOutput.setLockStatus(aiUserAlbumLockStatusMap.get(userAlbum.getId())); + if (LockStatusEnum.LOCK == aiUserAlbumLockStatusMap.get(userAlbum.getId())) { + //上锁,显示模糊图片 + BlurryRecordOutput blurryRecordOutput = blurryRecordOutputMap.get(userAlbum.getId()); + if (blurryRecordOutput == null) { + log.info("===> blurryRecordOutput is null, id : {}", userAlbum.getId()); + return; + } + albumOutput.setImg1(blurryRecordOutput.getImg1()); + albumOutput.setImg2(blurryRecordOutput.getImg2()); + albumOutput.setImg3(blurryRecordOutput.getImg3()); + return; + } + //解锁,需要ip和过期时间签名的图片 + String fileUrl = userAlbum.getImgUrl(); + //计算图片的失效时间从当前时间开始+12小时 + Long expTime = System.currentTimeMillis() + 12 * 60 * 60 * 1000L; + //如果是ipv4 则把日期和ip一起签名 + String img1Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.IMG_468_600, new Date(expTime), ipAddress); + String img2Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.IMG_800_800, new Date(expTime), ipAddress); + String img3Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.ORG_IMG, new Date(expTime), ipAddress); + albumOutput.setImg1(img1Url); + albumOutput.setImg2(img2Url); + albumOutput.setImg3(img3Url); + } catch (Exception e) { + log.error("===> blurImgHandle error : ", e); + ToastResultCode.SYS_SYSTEM_EXCEPTION.check(true); + } + } + + @Override + public void addDefaultAlbum(Long currentUserId, Long aiId, String imageUrl, String width, String height) { + //添加默认相册前,需要所之前的默认图片置为非默认 + update(Wrappers.lambdaUpdate() + .eq(AiUserAlbum::getAiId, aiId) + .eq(AiUserAlbum::getIsDefault, true) + .set(AiUserAlbum::getIsDefault, false) + ); + //添加保存默认相册图片 + AiUserAlbum aiUserAlbum = AiUserAlbum.builder() + .imgUrl(imageUrl) + .sourceImgUrl(imageUrl) + .width(width) + .height(height) + .aiId(aiId) + .userId(currentUserId) + .isDelete(false) + .isDefault(true) + .unlockPrice(0L) + .createTime(LocalDateTime.now()) + .editTime(LocalDateTime.now()) + .build(); + save(aiUserAlbum); + //该图片替换为虚拟角色的个人主页头图,卡片主图,聊天背景(但不影响用户自定义设置的聊天背景)-需求变更-7.9 + aiUserSetService.setHomeImageUrl(aiId, imageUrl); + } + + @Override + public void setAlbumUnlockPrice(Long currentUserId, SetAlbumUnlockPriceInput input) { + //权限检验,只有主人才能修改 + AiUserAlbum aiUserAlbum = getOne(Wrappers.lambdaQuery().eq(AiUserAlbum::getId, input.getAlbumId())); + ToastResultCode.SYS_PERMISSION_DENIED.check(aiUserAlbum == null); + ToastResultCode.SYS_PERMISSION_DENIED.check(!aiUserAlbum.getUserId().equals(currentUserId)); + ToastResultCode.ALBUM_DEFAULT_CAN_NOT_MODIFY_PAY.check(aiUserAlbum.getIsDefault()); + List copyAndBlurryImgInputList = Lists.newArrayList(); + //解锁价格大于0 需要模糊处理 + if (input.getUnlockPrice() > 0) { + //判断图片是否已经生成模糊图片 + Map blurryRecordOutputMap = copyAndBlurryImgClient.blurryRecordBizIdMap(BlurryImgBizTypeEnum.ALBUM.name(), Lists.newArrayList(aiUserAlbum.getId())); + if (blurryRecordOutputMap == null || blurryRecordOutputMap.get(aiUserAlbum.getId()) == null) { + CopyAndBlurryImgInput copyAndBlurryImgInput = new CopyAndBlurryImgInput(); + copyAndBlurryImgInput.setSourceFileUrl(aiUserAlbum.getSourceImgUrl()); + copyAndBlurryImgInput.setUserId(currentUserId); + copyAndBlurryImgInput.setBizType(BlurryImgBizTypeEnum.ALBUM); + copyAndBlurryImgInput.setBizId(aiUserAlbum.getId()); + copyAndBlurryImgInputList.add(copyAndBlurryImgInput); + } + if (CollectionUtils.isNotEmpty(copyAndBlurryImgInputList)) { + //调用oss服务内部api模糊处理 + Map blurryAlbumImgMap = copyAndBlurryImgClient.copyAndBlurryAlbumImg(copyAndBlurryImgInputList); + List updateUserAlbumList = Lists.newArrayList(); + for (CopyAndBlurryImgInput copyAndBlurryImgInput : copyAndBlurryImgInputList) { + Long bizId = copyAndBlurryImgInput.getBizId(); + AiUserAlbum userAlbum = new AiUserAlbum(); + userAlbum.setId(bizId); + userAlbum.setImgUrl(blurryAlbumImgMap.get(bizId)); + updateUserAlbumList.add(userAlbum); + } + //批量更新 + if (CollectionUtils.isNotEmpty(updateUserAlbumList)) { + updateBatchById(updateUserAlbumList); + } + } else { + //更新图片地址 + update(Wrappers.lambdaUpdate().set(AiUserAlbum::getImgUrl, aiUserAlbum.getImgUrl()).eq(AiUserAlbum::getId, input.getAlbumId())); + } + } else { + //之前是付费,设置成免费,图片地址要恢复成源图片 + update(Wrappers.lambdaUpdate().set(AiUserAlbum::getImgUrl, aiUserAlbum.getSourceImgUrl()).eq(AiUserAlbum::getId, input.getAlbumId())); + } + //更新相册解锁价格 + update(Wrappers.lambdaUpdate().set(AiUserAlbum::getUnlockPrice, input.getUnlockPrice()).eq(AiUserAlbum::getId, input.getAlbumId())); + } + + @Override + public ViewUnlockAlbumImgOutput unlockAlbumImg(Long userId, UnlockAlbumImgInput input) { + Long albumId = input.getAlbumId(); + AiUserAlbum aiUserAlbum = getOne(Wrappers.lambdaQuery().eq(AiUserAlbum::getId, albumId)); + ToastResultCode.ALBUM_NOT_EXIST.check(aiUserAlbum == null); + //判断Ai是否存在 + AiUser aiUser = aiUserSearchService.getAiUserByAiId(input.getAiId()); + ToastResultCode.AI_USER_NOT_EXIST.check(aiUser == null); + //ai是否删除或设置成私密 + Boolean isDelete = aiUser.getIsDelete(); + //解锁价格 + Long unlockPrice = aiUserAlbum.getUnlockPrice(); + + ViewUnlockAlbumImgOutput output = new ViewUnlockAlbumImgOutput(); + //已解锁,不执行 + Boolean unlockAlbumId = aiUserAlbumUnlockService.isUnlockAlbumId(userId, aiUserAlbum.getId()); + if (unlockAlbumId) { + output.setImg1(aiUserAlbum.getSourceImgUrl()); + output.setImg2(aiUserAlbum.getSourceImgUrl()); + output.setImg3(aiUserAlbum.getSourceImgUrl()); + return output; + } + //价格大于0时,才处理 + if (unlockPrice > 0L) { + //平台抽取50%作为手续费,向下取整,平台少收服务费 + Long platformFee = new BigDecimal(unlockPrice).multiply(PLATFORM_RATE).divide(new BigDecimal(100),0, RoundingMode.FLOOR).multiply(new BigDecimal(100)).longValue(); + //生成订单编号 + String orderNo = KeyGenerator.instance().generatorUniqueKey(""); + //调用支付服务扣款 + BalanceCheckoutInput balanceCheckoutInput = BalanceCheckoutInput.builder() + .platform("Balance") + .outTradeNo(orderNo) +// .bizType(aiUser.getIsDelete() ? BizType.IMAGE_UNLOCK_TO_P : BizType.IMAGE_UNLOCK) + .bizType(BizType.IMAGE_UNLOCK) + .name("图片解锁") + //付款人 + .srcAccountId(userId) + //收款人 + .desAccountId(aiUserAlbum.getUserId()) + //礼物总金额 + .productAmount(unlockPrice) + //平台抽取的金额 + .platformFee(isDelete ? unlockPrice : platformFee) + //折扣金额 + .promoAmount(0L) + .build(); + payClient.checkoutToUser(balanceCheckoutInput); + + //保存解锁记录 + aiUserAlbumUnlockService.addUnlockAlbumRecord(userId, albumId, orderNo); + //保存关系数据 + aiUserHeartbeatRelationService.initAiUserHeartbeatRelation(userId, input.getAiId()); +// //绑定meet关系(直接写到数据库中) +// userAiMeetService.addMeet(userId, input.getAiId()); + //如果有消息的话,需要修改消息内容 + if (input.getMessageServerId() != null) { + UpdateAiSendCustomImageMessageInput updateAiSendCustomImageMessageInput = UpdateAiSendCustomImageMessageInput.builder() + .messageServerId(input.getMessageServerId()) + .userId(userId) + .aiId(aiUserAlbum.getAiId()) + .attachment(AttachBo.builder() + .type("IMAGE") + .unlockPrice(0L) + .url(aiUserAlbum.getSourceImgUrl()) + .height(aiUserAlbum.getHeight()) + .width(aiUserAlbum.getWidth()) + .build()) + .build(); + imMessageClient.updateAiSendCustomImageMessage(updateAiSendCustomImageMessageInput); + } + + //ai未删除且不是设置私密才发送系统通知,ai Coin数统计 + if (!isDelete) { + //解锁图片后发送系统通知 + //用户实际所得金额 + commonMessageService.unlockAlbumImgSendMessage(userId, aiUserAlbum.getAiId(), aiUserAlbum.getId(), unlockPrice); + //发送ai ai图片解锁赚取,Coin数统计 + commonSendMqService.aiUserStatMq(aiUserAlbum.getAiId(), AiUserStatPayload.Type.UNLOCK_IMG, unlockPrice); + } + //解锁后返回图片可访问 + output = viewUnlockAlbumImg(userId, ViewUnlockAlbumImgInput.builder().aiId(aiUserAlbum.getAiId()).albumId(albumId).build()); + } else { + //价格为0,免费时直接返源图给用户 + output.setImg1(aiUserAlbum.getSourceImgUrl()); + output.setImg2(aiUserAlbum.getSourceImgUrl()); + output.setImg3(aiUserAlbum.getSourceImgUrl()); + } + return output; + } + + @Override + public ViewUnlockAlbumImgOutput viewUnlockAlbumImg(Long currentUserId, ViewUnlockAlbumImgInput input) { + Long albumId = input.getAlbumId(); + AiUserAlbum aiUserAlbum = getOne(Wrappers.lambdaQuery().eq(AiUserAlbum::getId, albumId)); + ToastResultCode.ALBUM_NOT_EXIST.check(aiUserAlbum == null); + //判断用户是否解锁该图片 + aiUserAlbumUnlockService.checkAlbumLockStatus(currentUserId, aiUserAlbum); + //解锁,需要ip和过期时间签名的图片 + String fileUrl = aiUserAlbum.getImgUrl(); + //计算图片的失效时间从当前时间开始+12小时 + Long expTime = System.currentTimeMillis() + 12 * 60 * 60 * 1000L; + String ipAddress = input.getIpAddress(); + //如果是ipv4 则把日期和ip一起签名 + String img1Url = null; + String img2Url = null; + String img3Url = null; + try { + img1Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.IMG_468_600, new Date(expTime), ipAddress); + img2Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.IMG_800_800, new Date(expTime), ipAddress); + img3Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.ORG_IMG, new Date(expTime), ipAddress); + } catch (Exception e) { + log.error("viewUnlockAlbumImg 图片签名失败", e); + } + ViewUnlockAlbumImgOutput output = new ViewUnlockAlbumImgOutput(); + output.setImg1(img1Url); + output.setImg2(img2Url); + output.setImg3(img3Url); + + //如果有消息的话,需要修改消息内容 + if (input.getMessageServerId() != null) { + UpdateAiSendCustomImageMessageInput updateAiSendCustomImageMessageInput = UpdateAiSendCustomImageMessageInput.builder() + .messageServerId(input.getMessageServerId()) + .userId(currentUserId) + .aiId(aiUserAlbum.getAiId()) + .attachment(AttachBo.builder() + .type("IMAGE") + .unlockPrice(0L) + .url(aiUserAlbum.getSourceImgUrl()) + .height(aiUserAlbum.getHeight()) + .width(aiUserAlbum.getWidth()) + .build()) + .build(); + imMessageClient.updateAiSendCustomImageMessage(updateAiSendCustomImageMessageInput); + } + + return output; + } + + @Override + public AIUserAlbumApiOutput getRandomLockImage(GetRandomLockImageInput input) throws InvalidKeyException { + List imageList = aiUserAlbumDao.getRandomLockImage(input.getUserId(), input.getAiId()); + if (CollectionUtils.isEmpty(imageList)) { + return null; + } + //随机处理 + Collections.shuffle(imageList); + AIUserAlbumApiOutput output = imageList.get(0); + //计算图片的失效时间从当前时间开始+ 10年 + Long expTime = System.currentTimeMillis() + 3650 * 24 * 60 * 60 * 1000L; + if (output.getUserId().equals(input.getUserId())) { + //图片不加密 + output.setImgUrl(cloudFrontSignerUtils.signer(output.getImgUrl() + CloudFrontSignerUtils.IMG_800_800, new Date(expTime), null)); + //价格设置为0 + output.setUnlockPrice(0L); + } else { + output.setImgUrl(cloudFrontSignerUtils.signer(output.getImgUrl() + CloudFrontSignerUtils.BLURRY_IMG_800_800, new Date(expTime), null)); + } + return output; + } + +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserAlbumUnlockServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserAlbumUnlockServiceImpl.java new file mode 100644 index 0000000..402a902 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserAlbumUnlockServiceImpl.java @@ -0,0 +1,127 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.sonic.common.enums.AppEnv; +import com.sonic.frog.dao.AiUserAlbumDao; +import com.sonic.frog.dao.AiUserAlbumUnlockDao; +import com.sonic.frog.domain.entity.AiUserAlbum; +import com.sonic.frog.domain.entity.AiUserAlbumUnlock; +import com.sonic.frog.enums.LockStatusEnum; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.service.AiUserAlbumUnlockService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @Author zzhan + * @Description 查询话题是否对访问用户解锁 + * @Date 2023/8/28 13:57 + * @Version 1.0 + */ +@Slf4j +@Service +public class AiUserAlbumUnlockServiceImpl extends ServiceImpl implements AiUserAlbumUnlockService { + + @Value("${spring.profiles.active}") + private String runMode; + + /** + * TODO 这里配置的是QA环境的 + */ + private static final List OFFICIAL_UN_LOCK_USER_ID_LIST_TEST = Lists.newArrayList(); + + /** + * TODO 这里配置他们要的官方号【线上环境】 + */ + private static final List OFFICIAL_UN_LOCK_USER_ID_LIST_PROD = Lists.newArrayList(); + + + /** + * 获取官方号的配置 + * + * @return + */ + private List getOfficialUnLockUserIdList() { + if (AppEnv.product.name().equals(runMode)) { + return OFFICIAL_UN_LOCK_USER_ID_LIST_PROD; + } + return OFFICIAL_UN_LOCK_USER_ID_LIST_TEST; + } + + + @Override + public Map queryAlbumUnlock(Long currentUserId, List aiUserAlbumList) { + if (CollectionUtils.isEmpty(aiUserAlbumList)) { + return Maps.newHashMap(); + } + //批量查询用户是对相册图片付费解锁 + List albumIds = aiUserAlbumList.stream().map(AiUserAlbum::getId).collect(Collectors.toList()); + List list = list(Wrappers.lambdaQuery().eq(AiUserAlbumUnlock::getUserId, currentUserId).in(AiUserAlbumUnlock::getAlbumId, albumIds)); + List unlockAlbumIds = list.stream().map(AiUserAlbumUnlock::getAlbumId).collect(Collectors.toList()); + //解锁状态map + Map aiUserAlbumLockStatusMap = Maps.newHashMap(); + for (AiUserAlbum aiUserAlbum : aiUserAlbumList) { + Long userId = aiUserAlbum.getUserId(); + Long id = aiUserAlbum.getId(); + //如果没有加锁,默认为null + if (aiUserAlbum.getUnlockPrice() == 0L) { + aiUserAlbumLockStatusMap.put(id, null); + continue; + } + //官方用户访问,解锁 + if (getOfficialUnLockUserIdList().contains(currentUserId)) { + aiUserAlbumLockStatusMap.put(id, LockStatusEnum.UNLOCK); + continue; + } + //是相册主人访问,解锁 + if (userId.equals(currentUserId)) { + aiUserAlbumLockStatusMap.put(id, LockStatusEnum.UNLOCK); + continue; + } + //查看用户是否对相册图片付费解锁 + if (unlockAlbumIds.contains(id)) { + aiUserAlbumLockStatusMap.put(id, LockStatusEnum.UNLOCK); + continue; + } + aiUserAlbumLockStatusMap.put(id, LockStatusEnum.LOCK); + } + return aiUserAlbumLockStatusMap; + } + + @Override + public void checkAlbumLockStatus(Long currentUserId, AiUserAlbum aiUserAlbum) { + Map aiUserAlbumLockStatusMap = queryAlbumUnlock(currentUserId, Lists.newArrayList(aiUserAlbum)); + //提示是否有操作权限 + ToastResultCode.SYS_PERMISSION_DENIED.check(LockStatusEnum.LOCK == aiUserAlbumLockStatusMap.get(aiUserAlbum.getId())); + } + + @Override + public List getUnlockAlbumIds(Long currentUserId) { + List list = list(Wrappers.lambdaQuery().eq(AiUserAlbumUnlock::getUserId, currentUserId)); + return list.stream().map(AiUserAlbumUnlock::getAlbumId).collect(Collectors.toList()); + } + + @Override + public void addUnlockAlbumRecord(Long currentUserId, Long albumId, String orderNo) { + AiUserAlbumUnlock aiUserAlbumUnlock = AiUserAlbumUnlock.builder().albumId(albumId).userId(currentUserId).orderNo(orderNo).build(); + save(aiUserAlbumUnlock); + } + + @Override + public Boolean isUnlockAlbumId(Long currentUserId, Long albumId) { + int count = count(Wrappers.lambdaQuery().eq(AiUserAlbumUnlock::getUserId, currentUserId).eq(AiUserAlbumUnlock::getAlbumId, albumId)); + return count > 0; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserExtServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserExtServiceImpl.java new file mode 100644 index 0000000..8a8673b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserExtServiceImpl.java @@ -0,0 +1,70 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.AiUserExtDao; +import com.sonic.frog.domain.entity.AiUserExt; +import com.sonic.frog.domain.input.AiUserExtInput; +import com.sonic.frog.service.AiUserExtService; +import com.sonic.frog.utils.BeanConver; +import io.jsonwebtoken.lang.Collections; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * AI用户扩展表服务实现类 + */ +@Service +@Slf4j +public class AiUserExtServiceImpl extends ServiceImpl implements AiUserExtService { + @Override + public void saveOrUpdateAiUserExt(Long aiId, AiUserExt aiUserExt, AiUserExtInput input, Long currentUserId) { + //不储存参考图片 + input.setImageReferenceUrl(null); + if (aiUserExt == null) { + // 新增 + aiUserExt = BeanConver.copeBean(input, AiUserExt.class); + aiUserExt.setAiId(aiId); + aiUserExt.setUserId(currentUserId); + aiUserExt.setCreateTime(LocalDateTime.now()); + aiUserExt.setEditorId(currentUserId); + aiUserExt.setEditTime(LocalDateTime.now()); + save(aiUserExt); + } else { + //编辑 + Long id = aiUserExt.getId(); + aiUserExt = BeanConver.copeBean(input, AiUserExt.class); + aiUserExt.setId(id); + aiUserExt.setEditorId(currentUserId); + aiUserExt.setEditTime(LocalDateTime.now()); + updateById(aiUserExt); + } + } + + @Override + public AiUserExt getAiUserExtByAiId(Long aiId) { + return getOne(Wrappers.lambdaQuery().eq(AiUserExt::getAiId, aiId).last("limit 1")); + } + + @Override + public void delAiUser(Long aiId) { + update(Wrappers.lambdaUpdate().eq(AiUserExt::getAiId, aiId).set(AiUserExt::getIsDelete, true)); + } + + @Override + public void updateAiUserDialoguePrologueSound(String url, Long aiId) { + update(Wrappers.lambdaUpdate().eq(AiUserExt::getAiId, aiId).set(AiUserExt::getDialoguePrologueSound, url)); + } + + @Override + public void updateAiUserSupportingContent(List supContentList, Long aiId) { + if (Collections.isEmpty(supContentList)) { + return; + } + update(Wrappers.lambdaUpdate().eq(AiUserExt::getAiId, aiId).set(AiUserExt::getSupportingContent, JSON.toJSONString(supContentList))); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserGiftServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserGiftServiceImpl.java new file mode 100644 index 0000000..7ad12cf --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserGiftServiceImpl.java @@ -0,0 +1,209 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.common.rpc.Page; +import com.sonic.daosupport.converter.PageConverter; +import com.sonic.frog.dao.AiUserGiftDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.AiUserGift; +import com.sonic.frog.domain.entity.GiftDict; +import com.sonic.frog.domain.input.AiUserGiftListInput; +import com.sonic.frog.domain.input.SendGiftInput; +import com.sonic.frog.domain.output.AiUserGiftListOutput; +import com.sonic.frog.domain.output.AiUserHeartbeatRelationOutput; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import com.sonic.frog.enums.MessageTypeEnum; +import com.sonic.frog.enums.SendGiftSceneEnum; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.event.inner.payload.AiUserStatPayload; +import com.sonic.frog.event.inner.payload.CalcHeartbeatLevelPayload; +import com.sonic.frog.event.outer.payload.AiChatPayload; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.KeyGenerator; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.client.SubscribeClient; +import com.sonic.lion.lib.enums.BizType; +import com.sonic.lion.lib.input.BalanceCheckoutInput; +import com.sonic.pigeon.lib.bo.ScoreExtension; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.enums.ImMessageTypeEnum; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.sonic.frog.enums.Constants.*; + +/** + * AI用户收到礼物业务实现类 + */ +@Service +@Slf4j +public class AiUserGiftServiceImpl extends ServiceImpl implements AiUserGiftService { + + @Autowired + private AiUserGiftDao aiUserGiftDao; + @Autowired + private GiftDictService giftDictService; + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private GiftRewardRecordService giftRewardRecordService; + @Autowired + private CommonMessageService commonMessageService; + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private HeartbeatLevelDictService heartbeatLevelDictService; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private UserAiMeetService userAiMeetService; + @Autowired + private PayClient payClient; + @Autowired + private SubscribeClient subscribeClient; + + @Override + public Page getAiUserGiftList(AiUserGiftListInput input) { + //分页获取 + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(input.getPage().getPn(), input.getPage().getPs()); + IPage aiUserGiftList = aiUserGiftDao.getAiUserGiftList(page, input.getAiId()); + return PageConverter.convert(aiUserGiftList, aiUserGiftListOutput -> { + return aiUserGiftListOutput; + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void sendGift(Long currentUserId, SendGiftInput input) { + Long giftId = input.getGiftId(); + Integer num = input.getNum(); + Long aiId = input.getAiId(); + SendGiftSceneEnum scene = input.getScene(); + //判断Ai是否存在 + AiUser aiUser = aiUserSearchService.getAiUserByAiId(aiId); + ToastResultCode.AI_USER_NOT_EXIST.check(aiUser == null); + //ai是否删除或设置成私密 + Boolean isDelete = aiUser.getIsDelete(); + //判断礼物是否存在 + GiftDict giftDict = giftDictService.getById(giftId); + ToastResultCode.GIFT_NOT_EXIST.check(giftDict == null); + //送该礼物需要的心动等级 + HeartbeatLevelEnum needHeartbeatLevel = giftDict.getHeartbeatLevel(); + if (needHeartbeatLevel != null) { + //获取用户与Ai的关动等级 + HeartbeatLevelEnum userHeartbeatLevel = aiUserHeartbeatRelationService.getHeartbeatLevel(currentUserId, aiId); + ToastResultCode.SYS_PERMISSION_DENIED.check(userHeartbeatLevel == null); + //判断等级是否满足 + List unlockList = heartbeatLevelDictService.getUnlockListByCode(userHeartbeatLevel); + ToastResultCode.SYS_PERMISSION_DENIED.check(CollectionUtils.isNotEmpty(unlockList) && !unlockList.contains(needHeartbeatLevel)); + } + //是否会员礼物,检测是否是会员 + Boolean isMember = subscribeClient.queryUserIsSubscribe(currentUserId); + ToastResultCode.SYS_PERMISSION_DENIED.check(giftDict.getIsMemberGift() && (isMember == null || !isMember)); + + Long totalAmount = 0L; + if (giftDict.getPrice() != null && giftDict.getPrice() > 0) { + totalAmount = Long.valueOf(giftDict.getPrice() * num); + } + + //平台抽取50%作为手续费,向下取整,平台少收服务费,自己给自己ai打赏抽成比例为0.5 + Long platformFee = new BigDecimal(totalAmount).multiply(PLATFORM_RATE).divide(new BigDecimal(100), 0, RoundingMode.FLOOR).multiply(new BigDecimal(100)).longValue(); + //生成订单编号 + String orderNo = KeyGenerator.instance().generatorUniqueKey(""); + //调用支付服务扣款 + BalanceCheckoutInput balanceCheckoutInput = BalanceCheckoutInput.builder() + .platform("Balance") + .outTradeNo(orderNo) +// .bizType(aiUser.getIsDelete() ? BizType.GIFT_TO_P : BizType.GIFT) + .bizType(BizType.GIFT) + .name("礼物支付") + //付款人 + .srcAccountId(currentUserId) + //收款人 + .desAccountId(aiUser.getUserId()) + //礼物总金额 + .productAmount(totalAmount) + //平台抽取的金额 + .platformFee(isDelete ? totalAmount : platformFee) + //折扣金额 + .promoAmount(0L) + .extend(JSON.toJSONString(input)) + .build(); + payClient.checkoutToUser(balanceCheckoutInput); + //添加礼物记录 + giftRewardRecordService.addGiftRewardRecord(giftDict, orderNo, currentUserId, aiId, num); + //增加AI收到该礼物的数量 + aiUserGiftDao.increaseGiftNum(aiId, giftId, num); + + boolean isMeet = false; + //home发送礼物场景 + if (SendGiftSceneEnum.HOME == scene) { + //绑定meet关系(直接写到数据库中) + userAiMeetService.addMeet(currentUserId, input.getAiId()); + } + //IM发送礼物场景 + if (SendGiftSceneEnum.IM == scene) { + //心动值 0.2心动值 = 1CrushCoin + BigDecimal heartbeatVal = ONE_COIN_HEARTBEAT_VAL.multiply(new BigDecimal(totalAmount)).divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP); + //计算总价,保留2位小数 + BigDecimal totalAmountBigDecimal = new BigDecimal(totalAmount).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP); + //发送IM礼物消息 +// String imContent = String.format("送你%s个%s礼物", num, giftDict.getName()); + String imContent = String.format(ImMessageTypeEnum.IM_SEND_GIFT.getContent(), num, giftDict.getName()); + Map extra = new HashMap<>(); + extra.put("giftNum", num); + extra.put("giftName", giftDict.getName()); + extra.put("giftId", giftId); + extra.put("giftIcon", giftDict.getIcon()); + extra.put("type", ImMessageTypeEnum.IM_SEND_GIFT.name()); + extra.put("title", imContent); + SendAiCustomerMessageInput sendAiCustomerMessageInput = SendAiCustomerMessageInput.builder() + .fromUserId(currentUserId) + .toUserId(aiId) + .content(imContent) + .attachment(JSON.toJSONString(extra)) + .extension(JSONObject.toJSONString(ScoreExtension.builder().score(heartbeatVal).build())) + .build(); + imMessageClient.sendUserToAiCustomerMessage(sendAiCustomerMessageInput); + //改写文本内容 +// imContent = String.format("送你%s个%s礼物,价值%s【这是真实付费礼物,不是说说而已】", num, giftDict.getName(), totalAmountBigDecimal); + imContent = String.format("I'm sending you %s %s gifts, worth %s. (These are real paid gifts, not just empty words.)", num, giftDict.getName(), totalAmountBigDecimal); + //发送AI消息回复MQ + commonSendMqService.sendAiChatMq(currentUserId, aiId, imContent, MessageTypeEnum.TEXT, null, AiChatPayload.SourceType.GIFT); + //发送礼物消息进行心动值计算MQ + if (totalAmount > 0) { + commonSendMqService.calHeartbeatLevelSendMq(CalcHeartbeatLevelPayload.builder() + .userId(currentUserId) + .aiId(aiId) + .heartbeatVal(heartbeatVal) + .type(CalcHeartbeatLevelPayload.Type.SEND_GIFT) + .build()); + } + } + + //ai未删除才发送系统通知,ai Coin数统计 + if (!isDelete) { + //发送系统通知 通知创作者,自己给自己的ai打赏时,不用发送系统通知 + if (!currentUserId.equals(aiUser.getUserId())) { + commonMessageService.aiGiftSendMessage(currentUserId, aiId, giftDict.getName(), num, totalAmount); + } + //发送ai Coin数统计 + commonSendMqService.aiUserStatMq(aiId, AiUserStatPayload.Type.GIFT, totalAmount); + } + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserHeartbeatRankServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserHeartbeatRankServiceImpl.java new file mode 100644 index 0000000..8be0b45 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserHeartbeatRankServiceImpl.java @@ -0,0 +1,63 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.AiUserHeartbeatRankDao; +import com.sonic.frog.domain.entity.AiUserHeartbeatRank; +import com.sonic.frog.service.AiUserHeartbeatRankService; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; + +@Slf4j +@Service +public class AiUserHeartbeatRankServiceImpl extends ServiceImpl implements AiUserHeartbeatRankService { + + @Autowired + private AiUserHeartbeatRankDao aiUserHeartbeatRankDao; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @Override + public void incrementHeartbeatVal(Long userId, BigDecimal heartbeatVal) { + //查询基础数据 + AiUserHeartbeatRank aiUserHeartbeatRank = getOne(Wrappers.lambdaQuery().select(AiUserHeartbeatRank::getId).eq(AiUserHeartbeatRank::getUserId, userId)); + if(aiUserHeartbeatRank == null) { + aiUserHeartbeatRank = AiUserHeartbeatRank.builder() + .userId(userId) + .heartbeatValTotal(heartbeatVal) + .createTime(LocalDateTime.now()) + .editTime(LocalDateTime.now()) + .build(); + save(aiUserHeartbeatRank); + return; + } + //更新总值 + aiUserHeartbeatRankDao.incrementHeartbeatVal(aiUserHeartbeatRank.getId(), heartbeatVal); + } + + @Override + public BigDecimal getCurrentUserRank(Long userId) { + Double score = stringRedisTemplate.opsForZSet().score(redisKeyUtils.heartbeatValTotalRankKey(), userId.toString()); + if(score != null) { + Long count = stringRedisTemplate.opsForZSet().count(redisKeyUtils.heartbeatValTotalRankKey(), 0, 1001); + if(count == null) { + return new BigDecimal("1"); + } + count = score.intValue() == 1000 ? 1001 : count; + //计算榜单的占比 保留两位小数点;百分比计算公式=(当前用户的排名绝对值/符合条件的排名用户数量)* 100% + BigDecimal percent = new BigDecimal(score).divide(new BigDecimal(count), 4, RoundingMode.HALF_UP); + return percent; + } + return new BigDecimal("1"); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserHeartbeatRelationServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserHeartbeatRelationServiceImpl.java new file mode 100644 index 0000000..d35b324 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserHeartbeatRelationServiceImpl.java @@ -0,0 +1,562 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.common.rpc.Page; +import com.sonic.daosupport.converter.PageConverter; +import com.sonic.frog.dao.AiUserHeartbeatRelationDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.AiUserHeartbeatRelation; +import com.sonic.frog.domain.entity.BuyHeartbeatValueRecord; +import com.sonic.frog.domain.entity.HeartbeatLevelDict; +import com.sonic.frog.domain.input.BuyHeartbeatValInput; +import com.sonic.frog.domain.input.HeartbeatRelationListInput; +import com.sonic.frog.domain.input.HeartbeatRelationSwitchInput; +import com.sonic.frog.domain.output.*; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import com.sonic.frog.event.inner.payload.AiUserStatPayload; +import com.sonic.frog.event.inner.payload.CalcHeartbeatLevelPayload; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.BeanConver; +import com.sonic.frog.utils.KeyGenerator; +import com.sonic.frog.utils.LimitUtils; +import com.sonic.frog.utils.RedisKeyUtils; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.enums.BizType; +import com.sonic.lion.lib.input.BalanceCheckoutInput; +import com.sonic.pigeon.lib.client.ImConversationClient; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.enums.ImMessageTypeEnum; +import com.sonic.pigeon.lib.input.ConversationExtensionInput; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import com.sonic.pigeon.lib.input.UpdateConversationInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.sonic.frog.enums.Constants.HEARTBEAT_VAL_PRICE; + +/** + * 用户与AI的心动关系服务实现类 + */ +@Service +@Slf4j +public class AiUserHeartbeatRelationServiceImpl extends ServiceImpl implements AiUserHeartbeatRelationService { + + @Autowired + private UserService userService; + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private HeartbeatLevelDictService heartbeatLevelDictService; + @Autowired + private ImConversationClient imConversationClient; + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private BuyHeartbeatValueRecordService buyHeartbeatValueRecordService; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private LimitUtils limitUtils; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private AiUserHeartbeatRelationDao aiUserHeartbeatRelationDao; + @Autowired + private CommonMessageService commonMessageService; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private PayClient payClient; + + /** + * 获取用户与AI的心动关系 + * + * @param userId + * @param aiId + * @return + */ + @Override + public AiUserHeartbeatRelation getHeartbeatRelation(Long userId, Long aiId) { + return getOne(Wrappers.lambdaQuery() + .eq(AiUserHeartbeatRelation::getUserId, userId) + .eq(AiUserHeartbeatRelation::getAiId, aiId) + .last("limit 1")); + } + + @Override + public void initAiUserHeartbeatRelation(Long userId, Long aiId) { + AiUserHeartbeatRelation aiUserHeartbeatRelation = getHeartbeatRelation(userId, aiId); + if (aiUserHeartbeatRelation == null) { + //保存记录 + aiUserHeartbeatRelation = new AiUserHeartbeatRelation(); + aiUserHeartbeatRelation.setUserId(userId); + aiUserHeartbeatRelation.setAiId(aiId); + aiUserHeartbeatRelation.setCreateTime(LocalDateTime.now()); + aiUserHeartbeatRelation.setEditTime(LocalDateTime.now()); + save(aiUserHeartbeatRelation); + } + } + + @Override + public AiUserHeartbeatRelationOutput getAiUserHeartbeatRelation(Long userId, Long aiId) { + //心动关系 + AiUserHeartbeatRelation aiUserHeartbeatRelation = getHeartbeatRelation(userId, aiId); + AiUserHeartbeatRelationOutput output = new AiUserHeartbeatRelationOutput(); + if (aiUserHeartbeatRelation != null) { + output = BeanConver.copeBean(aiUserHeartbeatRelation, AiUserHeartbeatRelationOutput.class); + HeartbeatLevelEnum heartbeatLevel = aiUserHeartbeatRelation.getHeartbeatLevel(); + output.setHeartbeatLevelNum(0); + if (heartbeatLevel != null) { + output.setHeartbeatLevelNum(heartbeatLevel.getNum()); + +// //心动等级名称 +// HeartbeatLevelDict heartbeatLevelDict = heartbeatLevelDictService.getHeartbeatLevelDictByLevel(heartbeatLevel); +// output.setHeartbeatLevelName(heartbeatLevelDict.getName()); + //郭老师说 给等级描述,不给上面的东西 + output.setHeartbeatLevelName(heartbeatLevel.getRelationStage()); + } + //计算相识天数 + output.setDayCount(aiUserHeartbeatRelation.getDayCount()); + // + } + //心动值单价 + output.setPrice(HEARTBEAT_VAL_PRICE); + //用户基础信息 + BaseUserInfoOutput baseUserInfoOutput = userService.baseUserInfo(userId); + //Ai基础信息 + AiUser aiUser = aiUserSearchService.getAiUserByAiId(aiId); + output.setUserHeadImg(baseUserInfoOutput != null ? baseUserInfoOutput.getHeadImage() : null); + output.setAiHeadImg(aiUser != null ? aiUser.getHeadImg() : null); + return output; + } + + @Override + public HeartbeatLevelOutput getAiUserHeartbeatLevel(Long userId, Long aiId) { + HeartbeatLevelOutput output = new HeartbeatLevelOutput(); + //心动关系 + AiUserHeartbeatRelationOutput aiUserHeartbeatRelation = getAiUserHeartbeatRelation(userId, aiId); + output.setAiUserHeartbeatRelation(aiUserHeartbeatRelation); + //心动等级字典列表 + List heartbeatLevelDictOutputList = heartbeatLevelDictService.getHearbeatLevelDictList(aiUserHeartbeatRelation.getHeartbeatLevel()); + output.setHeartbeatLeveLDictList(heartbeatLevelDictOutputList); + return output; + } + + @Override + public HeartbeatLevelEnum getHeartbeatLevel(Long userId, Long aiId) { + AiUserHeartbeatRelation aiUserHeartbeatRelation = getHeartbeatRelation(userId, aiId); + return aiUserHeartbeatRelation != null ? aiUserHeartbeatRelation.getHeartbeatLevel() : null; + } + + @Override + public Boolean relationCheck(Long userId, Long aiId) { + int count = count(Wrappers.lambdaQuery() + .eq(AiUserHeartbeatRelation::getUserId, userId) + .eq(AiUserHeartbeatRelation::getAiId, aiId) + .last("limit 1")); + return count > 0; + } + + @Override + public void calcAiUserHeartbeatLevel(CalcHeartbeatLevelPayload payload) { + Long userId = payload.getUserId(); + Long aiId = payload.getAiId(); + BigDecimal calcHeartbeatVal = payload.getHeartbeatVal(); + CalcHeartbeatLevelPayload.Type type = payload.getType(); + if (userId == null || aiId == null || calcHeartbeatVal == null) { + return; + } + //心动关系获取,初始化 + AiUserHeartbeatRelation aiUserHeartbeatRelation = getHeartbeatRelation(userId, aiId); + if (aiUserHeartbeatRelation == null) { + aiUserHeartbeatRelation = new AiUserHeartbeatRelation(); + aiUserHeartbeatRelation.setUserId(userId); + aiUserHeartbeatRelation.setAiId(aiId); + aiUserHeartbeatRelation.setHeartbeatVal(new BigDecimal(0)); + aiUserHeartbeatRelation.setCreateTime(LocalDateTime.now()); + aiUserHeartbeatRelation.setEditTime(LocalDateTime.now()); + //首次聊天时间 + aiUserHeartbeatRelation.setFirstChatTime(LocalDateTime.now()); + save(aiUserHeartbeatRelation); + //发送ai会话人数统计 + commonSendMqService.aiUserStatMq(aiId, AiUserStatPayload.Type.FIRST_CHAT, null); + } + //聊天类型,发送AI聊天数统计MQ + if (CalcHeartbeatLevelPayload.Type.isChatType(type)) { + //发送ai聊天数统计 + commonSendMqService.aiUserStatMq(aiId, AiUserStatPayload.Type.CHAT, null); + } + //零值不处理 + if (calcHeartbeatVal.equals(BigDecimal.ZERO)) { + return; + } + //心动值 + BigDecimal subtractHeartbeatVal = aiUserHeartbeatRelation.getSubtractHeartbeatVal() != null ? aiUserHeartbeatRelation.getSubtractHeartbeatVal() : BigDecimal.ZERO; + //24小时未聊天扣减心动值 + if (CalcHeartbeatLevelPayload.Type.HOURS_WITHOUT_CHAT.equals(type)) { + //再次判断一下最后聊天时间,如果在24小时内,则不处理 + if (aiUserHeartbeatRelation.getLastChatTime() != null && aiUserHeartbeatRelation.getLastChatTime().plusHours(24).isAfter(LocalDateTime.now())) { + return; + } + //扣减的心动值要加 + subtractHeartbeatVal = subtractHeartbeatVal.add(calcHeartbeatVal); + //心动值要减 + calcHeartbeatVal = calcHeartbeatVal.multiply(new BigDecimal("-1")); + } + BigDecimal oldHeartbeatVal = aiUserHeartbeatRelation.getHeartbeatVal(); + BigDecimal heartbeatVal = aiUserHeartbeatRelation.getHeartbeatVal().add(calcHeartbeatVal); + //如果小于0了,则设置为0 + if (heartbeatVal.compareTo(new BigDecimal("0")) < 0) { + heartbeatVal = new BigDecimal("0"); + } + //根据心动值获取心动等级 + HeartbeatLevelDict heartbeatLevelDict = heartbeatLevelDictService.getHeartbeatLevelByVal(heartbeatVal); + HeartbeatLevelEnum heartbeatLevelEnum = heartbeatLevelDict != null ? heartbeatLevelDict.getCode() : null; + //原先等级 + HeartbeatLevelEnum oldHeartbeatLevelEnum = aiUserHeartbeatRelation.getHeartbeatLevel(); + HeartbeatLevelDict oldHeartbeatLevelDict = heartbeatLevelDictService.getHeartbeatLevelDictByLevel(oldHeartbeatLevelEnum); + //如果降级并且没有等级了,默认成1级 + if (heartbeatLevelEnum == null && HeartbeatLevelEnum.isDowngrade(oldHeartbeatLevelEnum, heartbeatLevelEnum)) { + heartbeatLevelEnum = HeartbeatLevelEnum.LEVEL_1; + } + //更新数据库 + update(Wrappers.lambdaUpdate() + .set(AiUserHeartbeatRelation::getHeartbeatLevel, heartbeatLevelEnum) + .set(AiUserHeartbeatRelation::getHeartbeatVal, heartbeatVal) + //扣减的心动值 + .set(CalcHeartbeatLevelPayload.Type.HOURS_WITHOUT_CHAT.equals(type), AiUserHeartbeatRelation::getSubtractHeartbeatVal, subtractHeartbeatVal) + .set(AiUserHeartbeatRelation::getEditTime, LocalDateTime.now()) + //聊天类型才更新最后一次聊天时间 + .set(CalcHeartbeatLevelPayload.Type.isChatType(type), AiUserHeartbeatRelation::getLastChatTime, LocalDateTime.now()) + //24小时未聊天扣减心动值,才更新最后一次扣减心动值时间 + .set(CalcHeartbeatLevelPayload.Type.HOURS_WITHOUT_CHAT.equals(type), AiUserHeartbeatRelation::getLastSubtractTime, LocalDateTime.now()) + .eq(AiUserHeartbeatRelation::getId, aiUserHeartbeatRelation.getId()) + ); + //等级升了,发送IM通知 + Boolean isSendImMessage = false; + String content = ""; + Map extra = new HashMap<>(); + if (HeartbeatLevelEnum.isUpgrade(oldHeartbeatLevelEnum, heartbeatLevelEnum)) { + isSendImMessage = true; + content = String.format(ImMessageTypeEnum.HEARTBEAT_LEVEL_UP.getContent(), heartbeatLevelDict.getName()); + extra.put("title", content); + + extra.put("type", ImMessageTypeEnum.HEARTBEAT_LEVEL_UP.name()); + } + //等级降了,发送IM通知 + if (HeartbeatLevelEnum.isDowngrade(oldHeartbeatLevelEnum, heartbeatLevelEnum)) { + isSendImMessage = true; + content = String.format(ImMessageTypeEnum.HEARTBEAT_LEVEL_DOWN.getContent(), oldHeartbeatLevelDict.getName()); + extra.put("title", content); + extra.put("type", ImMessageTypeEnum.HEARTBEAT_LEVEL_DOWN.name()); + //降级发送系统通知 发送时机 每个角色第一次降级时发送 + if (aiUserHeartbeatRelation.getIsFirstDowngrade() != null && aiUserHeartbeatRelation.getIsFirstDowngrade()) { + //发送降级通知 + commonMessageService.aiHeartbeatLevelDowngradeSendMessage(userId, aiId, heartbeatLevelEnum != null ? heartbeatLevelEnum.getLevelName() : HeartbeatLevelEnum.LEVEL_1.getLevelName()); + //更新数据库为不是第一次降级 + update(Wrappers.lambdaUpdate() + .set(AiUserHeartbeatRelation::getIsFirstDowngrade, false) + .eq(AiUserHeartbeatRelation::getId, aiUserHeartbeatRelation.getId()) + ); + } + } + if (isSendImMessage) { + extra.put("heartbeatLevel", heartbeatLevelEnum); + extra.put("heartbeatLevelNum", heartbeatLevelEnum != null ? heartbeatLevelEnum.getNum() : null); + extra.put("heartbeatVal", heartbeatVal); + extra.put("heartbeatLevelName", heartbeatLevelDict != null ? heartbeatLevelDict.getName() : null); + SendAiCustomerMessageInput sendAiCustomerMessageInput = SendAiCustomerMessageInput.builder() + .fromUserId(aiId) + .toUserId(userId) + .content(content) + .attachment(JSON.toJSONString(extra)) + .build(); + imMessageClient.sendAiToUserCustomerMessage(sendAiCustomerMessageInput); + + //直接删除 response 的缓存,通过聊天来进行自动构建更新等级的变更 + String redisKey = redisKeyUtils.chatResponseIdCacheKey(payload.getUserId(), payload.getAiId()); + log.info("===> del chatResponseIdCacheKey {}, {}, {}, {}, {}", payload.getUserId(), payload.getAiId(), oldHeartbeatLevelEnum, heartbeatLevelEnum, redisKey); + stringRedisTemplate.delete(redisKey); + //写入redis清理缓存任务 缓存3天 + stringRedisTemplate.opsForValue().set(redisKeyUtils.chatResponseIdClearTaskKey(payload.getUserId(), payload.getAiId()), "1", 3, TimeUnit.DAYS); + //写入关系等级缓存 + stringRedisTemplate.opsForValue().set(redisKeyUtils.aiUserHeartbeatLevelCacheKey(payload.getUserId(), aiId), heartbeatLevelEnum.name(), 3650, TimeUnit.DAYS); + } + //更新IM的关系标签 + imUpdateConversation(aiId, userId, heartbeatVal, heartbeatLevelEnum == null ? oldHeartbeatLevelEnum : heartbeatLevelEnum, aiUserHeartbeatRelation == null ? true : aiUserHeartbeatRelation.getIsShow()); + //心动分计算 每10分钟达到10次聊天频次就计算一次 + calcHeartbeatScoreMq(userId, aiId); + //计算心动分值榜单 + commonSendMqService.calcHeartbeatRankMq(aiId, userId, oldHeartbeatVal, heartbeatVal); + } + + /** + * 发送MQ计算心动分 + */ + private void calcHeartbeatScoreMq(Long userId, Long aiId) { + String redisKey = redisKeyUtils.calcHeartbeatScoreLimitKey(userId, aiId); + boolean b = limitUtils.defaultLimitCheckByKey(redisKey, 10, 10 * 60); + if (b) { + //发送MQ计算心动分 + commonSendMqService.calcHeartbeatScoreMq(userId, aiId); + //删除redis + stringRedisTemplate.delete(redisKey); + } + } + + /** + * 更新IM会话关系标签 + * + * @param aiId + * @param userId + * @param heartbeatVal + * @param heartbeatLevelEnum + * @param isShow + */ + private void imUpdateConversation(Long aiId, Long userId, BigDecimal heartbeatVal, HeartbeatLevelEnum heartbeatLevelEnum, Boolean isShow) { + //更新IM的关系标签 + UpdateConversationInput updateConversationInput = UpdateConversationInput.builder() + .fromUserId(userId) + .toUserId(aiId) + .extension(JSONObject.toJSONString(ConversationExtensionInput.builder() + .heartbeatVal(heartbeatVal) + .heartbeatLevel(heartbeatLevelEnum == null ? null : heartbeatLevelEnum.name()) + .isShow(isShow) + .build())) + .build(); + imConversationClient.updateConversation(updateConversationInput); + } + + + @Override + public void buyHeartbeatVal(Long userId, BuyHeartbeatValInput input) { + Long aiId = input.getAiId(); + //用户与AI关系 + AiUserHeartbeatRelation aiUserHeartbeatRelation = getHeartbeatRelation(userId, aiId); + if (aiUserHeartbeatRelation == null) { + return; + } + //购买的心动值数量 + BigDecimal heartbeatVal = aiUserHeartbeatRelation.getSubtractHeartbeatVal(); + //总金额 + BigDecimal totalAmount = heartbeatVal.multiply(new BigDecimal(HEARTBEAT_VAL_PRICE / 100)); + if (totalAmount.compareTo(new BigDecimal("1")) < 0) { + totalAmount = totalAmount.setScale(0, RoundingMode.HALF_UP); + } else { + totalAmount = totalAmount.setScale(0, RoundingMode.HALF_DOWN); + } + //下单,从用户帐户中扣款 + String orderNo = KeyGenerator.instance().generatorUniqueKey(""); + BalanceCheckoutInput balanceCheckoutInput = BalanceCheckoutInput.builder() + .platform("Balance") + .outTradeNo(orderNo) + .bizType(BizType.HEARTBEAT_PURCHASE) + .name(BizType.HEARTBEAT_PURCHASE.getDesc()) + //付款人 + .srcAccountId(userId) + //收款人 + .desAccountId(-1L) + //礼物总金额 + .productAmount(totalAmount.multiply(new BigDecimal(100L)).longValue()) + //折扣金额 + .promoAmount(0L) + .build(); + payClient.checkoutToB(balanceCheckoutInput); + //生成购买记录 + BuyHeartbeatValueRecord buyHeartbeatValueRecord = BuyHeartbeatValueRecord.builder() + .userId(userId).aiId(aiId) + .price(new BigDecimal(HEARTBEAT_VAL_PRICE)).totalAmount(totalAmount) + .heartbeatVal(heartbeatVal) + .orderNo(orderNo) + .createTime(LocalDateTime.now()) + .editTime(LocalDateTime.now()) + .build(); + buyHeartbeatValueRecordService.addBuyHeartbeatValueRecord(buyHeartbeatValueRecord); + //已扣减的心动值减去购买数量 + update(Wrappers.lambdaUpdate() + .set(AiUserHeartbeatRelation::getSubtractHeartbeatVal, aiUserHeartbeatRelation.getSubtractHeartbeatVal().subtract(heartbeatVal)) + .eq(AiUserHeartbeatRelation::getId, aiUserHeartbeatRelation.getId()) + ); + //发送MQ计算心动等级 + commonSendMqService.calHeartbeatLevelSendMq(CalcHeartbeatLevelPayload.builder() + .userId(userId) + .aiId(aiId) + .heartbeatVal(heartbeatVal) + .type(CalcHeartbeatLevelPayload.Type.BUY_HEARTBEAT_VAL) + .build()); + } + + @Override + public void heartbeatRelationSwitch(Long userId, HeartbeatRelationSwitchInput input) { + AiUserHeartbeatRelation aiUserHeartbeatRelation = getHeartbeatRelation(userId, input.getAiId()); + if (aiUserHeartbeatRelation != null) { + //更新开关 + update(Wrappers.lambdaUpdate() + .set(AiUserHeartbeatRelation::getIsShow, input.getIsShow()) + .eq(AiUserHeartbeatRelation::getId, aiUserHeartbeatRelation.getId()) + ); + //更新IM会话关系标签 + imUpdateConversation(input.getAiId(), userId, aiUserHeartbeatRelation.getHeartbeatVal(), aiUserHeartbeatRelation.getHeartbeatLevel(), input.getIsShow()); + } + } + + @Override + public void calcAiUserHeartbeatScore(Long userId, Long aiId) { + AiUserHeartbeatRelation aiUserHeartbeatRelation = getHeartbeatRelation(userId, aiId); + if (aiUserHeartbeatRelation == null) { + return; + } + //判断是否达到初识等级,未达到,直接返回 + HeartbeatLevelEnum heartbeatLevel = aiUserHeartbeatRelation.getHeartbeatLevel(); + if (heartbeatLevel == null) { + return; + } + //达到初识关系以及以上的心动值就可以用来与和这个虚拟角色对话过切达到初识关系及以上的其他对话者进行排名,排名/参与排名的所有用户*100%,保留两位小数就是百分比 + //获取最近90天当前用户与AI的心动值排名 + Integer userRank = aiUserHeartbeatRelationDao.getUserHeartbeatRank(userId, aiId); + //获取最近90天与该AI关系达到初识等级以上的人数 + Integer totalUserCount = aiUserHeartbeatRelationDao.getUserHeartbeatCount(aiId); + //心动分计算逻辑 + BigDecimal heartbeatScore = new BigDecimal("0.99"); + if (userRank != null && totalUserCount != null && totalUserCount > 1) { + heartbeatScore = new BigDecimal(1).subtract(new BigDecimal(userRank).divide(new BigDecimal(totalUserCount), 2, RoundingMode.HALF_UP)); + } + //更新到数据库 + update(Wrappers.lambdaUpdate() + .set(AiUserHeartbeatRelation::getHeartbeatScore, heartbeatScore) + .eq(AiUserHeartbeatRelation::getId, aiUserHeartbeatRelation.getId()) + ); + } + + @Override + public void subtractHeartbeatVal(Long userId) { + //查询该用户下所有的AI关系 + List list = list(Wrappers.lambdaQuery() + .eq(AiUserHeartbeatRelation::getUserId, userId) + .eq(AiUserHeartbeatRelation::getIsShow, true) + ); + if (CollectionUtils.isNotEmpty(list)) { + list.forEach(aiUserHeartbeatRelation -> { + //最后一次聊天时间 + LocalDateTime lastChatTime = aiUserHeartbeatRelation.getLastChatTime(); + //最后一次扣减心动值时间 + LocalDateTime lastSubtractTime = aiUserHeartbeatRelation.getLastSubtractTime(); + //看以哪个时间来计算 + LocalDateTime calcTime = lastChatTime; + if (lastSubtractTime != null) { + calcTime = lastChatTime.isBefore(lastSubtractTime) ? lastSubtractTime : lastChatTime; + } + //判断是否已超过24小时未聊天 + if (calcTime.plusHours(24).isBefore(LocalDateTime.now())) { + //已超过24小时未聊天,根据不同心动等级扣除不一样的心动值 + HeartbeatLevelEnum heartbeatLevelEnum = aiUserHeartbeatRelation.getHeartbeatLevel(); + //对应等级需要扣除的心动值 + BigDecimal subtractHeartbeatVal = heartbeatLevelEnum.getSubtractHeartbeatVal(); + //计算超过多少天没有聊天了 + long hours = Duration.between(calcTime, LocalDateTime.now()).toHours(); + long dayCount = hours / 24; // 按24小时算1天 + //总的需要扣除的心动值 + BigDecimal totalSubtractHeartbeatVal = subtractHeartbeatVal.multiply(new BigDecimal(dayCount)); + //发送到心动等级计算MQ + commonSendMqService.calHeartbeatLevelSendMq(CalcHeartbeatLevelPayload.builder() + .userId(userId) + .aiId(aiUserHeartbeatRelation.getAiId()) + .heartbeatVal(totalSubtractHeartbeatVal) + .type(CalcHeartbeatLevelPayload.Type.HOURS_WITHOUT_CHAT) + .build()); + } + }); + } + } + + @Override + public void withoutChatSubtractHeartbeatVal(Long userId) { + //24小时未聊天发送MQ到扣减心动值 +// commonSendMqService.subtractHeartbeatValMq(userId); + } + + @Override + public Page heartbeatRelationList(Long userId, HeartbeatRelationListInput input) { + input.setUserId(userId); + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(input.getPage().getPn(), input.getPage().getPs()); + IPage pageResult = aiUserHeartbeatRelationDao.heartbeatRelationListPage(page, input); + List records = pageResult.getRecords(); + List aiIdList = records.stream().map(AiUserHeartbeatRelation::getAiId).collect(Collectors.toList()); + //Ai基础信息Map + Map aiUserMap = aiUserSearchService.mapByAiIdList(aiIdList); + return PageConverter.convert(pageResult, aiUserHeartbeatRelation -> { + AiUserBaseOutput aiUserBaseOutput = aiUserMap.get(aiUserHeartbeatRelation.getAiId()); + //基础信息 + HeartbeatRelationListOutput output = BeanConver.copeBean(aiUserBaseOutput, HeartbeatRelationListOutput.class); + output.setAiId(aiUserHeartbeatRelation.getAiId()); + //业务信息 + output.setHeartbeatLevel(aiUserHeartbeatRelation.getHeartbeatLevel()); + output.setHeartbeatVal(aiUserHeartbeatRelation.getHeartbeatVal()); + output.setHeartbeatLevelNum(aiUserHeartbeatRelation.getHeartbeatLevel().getNum()); + output.setIsShow(aiUserHeartbeatRelation.getIsShow() == null ? true : aiUserHeartbeatRelation.getIsShow()); + return output; + }); + } + + @Override + public void hours24NoChatSubtractHeartbeatValJob() { + //24小时后 + LocalDateTime calcTime = LocalDateTime.now().minusHours(24); + //查询有心动值的,且最后扣减时间小于该时间段,且最后聊天时间小于该时间段的关系,每次取1000条数据 + List list = aiUserHeartbeatRelationDao.hours24NoChatRelationList(calcTime); + //发送扣减心动值MQ处理 + for (AiUserHeartbeatRelation aiUserHeartbeatRelation : list) { + Long userId = aiUserHeartbeatRelation.getUserId(); + Long aiId = aiUserHeartbeatRelation.getAiId(); + //已超过24小时未聊天,根据不同心动等级扣除不一样的心动值 + HeartbeatLevelEnum heartbeatLevelEnum = aiUserHeartbeatRelation.getHeartbeatLevel(); + if (heartbeatLevelEnum == null) { + continue; + } + //对应等级需要扣除的心动值 + BigDecimal subtractHeartbeatVal = heartbeatLevelEnum.getSubtractHeartbeatVal(); + //发送到心动等级计算MQ + commonSendMqService.calHeartbeatLevelSendMq(CalcHeartbeatLevelPayload.builder() + .userId(userId) + .aiId(aiId) + .heartbeatVal(subtractHeartbeatVal) + .type(CalcHeartbeatLevelPayload.Type.HOURS_WITHOUT_CHAT) + .build()); + } + } + + @Override + public Map queryAIHeartbeatVal(Long currentUserId, List aiIds) { + if (currentUserId == null || aiIds == null || aiIds.isEmpty()) { + return new HashMap<>(0); + } + List list = list(Wrappers.lambdaQuery() + .select(AiUserHeartbeatRelation::getAiId, AiUserHeartbeatRelation::getHeartbeatVal) + .eq(AiUserHeartbeatRelation::getUserId, currentUserId) + .in(AiUserHeartbeatRelation::getAiId, aiIds)); + Map aiHeartbeatValMap = new HashMap<>(aiIds.size()); + for (AiUserHeartbeatRelation aiUserHeartbeatRelation : list) { + aiHeartbeatValMap.put(aiUserHeartbeatRelation.getAiId(), aiUserHeartbeatRelation.getHeartbeatVal() == null ? BigDecimal.ZERO : aiUserHeartbeatRelation.getHeartbeatVal()); + } + return aiHeartbeatValMap; + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserSearchServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserSearchServiceImpl.java new file mode 100644 index 0000000..8fc1a05 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserSearchServiceImpl.java @@ -0,0 +1,287 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.sonic.frog.dao.AiUserDao; +import com.sonic.frog.domain.entity.*; +import com.sonic.frog.domain.input.SetChatBubbleInput; +import com.sonic.frog.domain.output.*; +import com.sonic.frog.enums.BubbleUnlockTypeEnum; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.BeanConver; +import com.sonic.frog.utils.BeanConvert; +import com.sonic.lion.lib.client.SubscribeClient; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.sonic.frog.enums.Constants.DEFAULT_BACKGROUND; + +/** + * AI用户表服务实现类 + */ +@Service +@Slf4j +public class AiUserSearchServiceImpl extends ServiceImpl implements AiUserSearchService { + + @Autowired + private AiDictService aiDictService; + @Autowired + private AiUserStatService aiUserStatService; + @Autowired + private AiUserExtService aiUserExtService; + @Autowired + private ChatSetService chatSetService; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private ChatBubbleDictService chatBubbleDictService; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private TimbreDictService timbreDictService; + @Autowired + private SubscribeClient subscribeClient; + @Autowired + private LikedService likedService; + @Autowired + private ChatService chatService; + @Autowired + private HeartbeatLevelDictService heartbeatLevelDictService; + + @Override + public AiUserBaseOutput getAiUserBaseInfo(Long currentUserId, Long aiId) { + //查询基础信息 + AiUser aiUser = getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, aiId)); + ToastResultCode.AI_USER_NOT_EXIST.check(aiUser == null); + //已经删除的,用户自己是不能看到的 + ToastResultCode.AI_USER_NOT_EXIST.check(aiUser.getIsDelete() && aiUser.getUserId().equals(currentUserId)); + //已经删除的、私密的,只有有关系的人才能看到 + if (aiUser.getIsDelete() || (aiUser.getPermission() == 2 && !aiUser.getUserId().equals(currentUserId))) { + //判断是否存在关系 + boolean isRelation = aiUserHeartbeatRelationService.relationCheck(currentUserId, aiId); + ToastResultCode.AI_USER_NOT_EXIST.check(!isRelation); + } + + AiUserBaseOutput output = BeanConver.copeBean(aiUser, AiUserBaseOutput.class); + + //性格,标签名称,角色名称 + Map aiDictNameMap = aiDictService.mapNameByCodeList(Sets.newHashSet(aiUser.getRoleCode(), aiUser.getCharacterCode(), aiUser.getTagCode())); + output.setCharacterName(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTagName(aiDictNameMap.get(aiUser.getTagCode())); + output.setRoleName(aiDictNameMap.get(aiUser.getRoleCode())); + //设置点赞数 + output.setLikedCount(aiUserStatService.getAiLikedCount(aiId)); + //获取当前用户是否点赞过 + if (currentUserId != null) { + output.setLiked(likedService.isLiked(currentUserId, aiId, Liked.BizType.AI)); + } + return output; + } + + @Override + public List getBaseUserAiList(Long userId) { + List list = list(Wrappers.lambdaQuery() + .eq(AiUser::getUserId, userId) + .eq(AiUser::getIsDelete, false) + .orderByDesc(AiUser::getId) + ); + if (CollectionUtils.isEmpty(list)) { + return Collections.emptyList(); + } + //获取字典数据 + Map aiDictNameMap = mapNameByCodeList(list); + List aiIdList = list.stream().map(AiUser::getAiId).collect(Collectors.toList()); + //批量获取点赞数 + Map likedCountMap = aiUserStatService.queryAiLikedCount(aiIdList); + + List outputList = Lists.newArrayList(); + for (AiUser aiUser : list) { + AiUserBaseListOutput output = BeanConvert.copeBean(aiUser, AiUserBaseListOutput.class); + //组装字典数据 + output.setRoleName(aiDictNameMap.get(aiUser.getRoleCode())); + output.setCharacterName(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTagName(aiDictNameMap.get(aiUser.getTagCode())); + output.setLikedNum(likedCountMap.get(aiUser.getAiId())); + outputList.add(output); + } + return outputList; + } + + @Override + public List getTargetUserAiList(Long userId) { + List list = list(Wrappers.lambdaQuery().eq(AiUser::getUserId, userId) + .eq(AiUser::getPermission, 1) + .eq(AiUser::getIsDelete, false)); + //获取字典数据 + Map aiDictNameMap = mapNameByCodeList(list); + List outputList = Lists.newArrayList(); + for (AiUser aiUser : list) { + AiUserTargetListOutput output = BeanConvert.copeBean(aiUser, AiUserTargetListOutput.class); + //组装字典数据 + output.setRoleName(aiDictNameMap.get(aiUser.getRoleCode())); + output.setCharacterName(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTagName(aiDictNameMap.get(aiUser.getTagCode())); + outputList.add(output); + } + return outputList; + } + + /** + * 获取Ai字典数据 + * + * @param list + * @return + */ + @Override + public Map mapNameByCodeList(List list) { + //获取字典数据 + Set roleCodeList = list.stream().map(AiUser::getRoleCode).collect(Collectors.toSet()); + Set characterCodeList = list.stream().map(AiUser::getCharacterCode).collect(Collectors.toSet()); + Set tagCodeList = list.stream().map(AiUser::getTagCode).collect(Collectors.toSet()); + //批量查询字典数据数据 + Set codeList = Sets.newHashSet(); + codeList.addAll(roleCodeList); + codeList.addAll(characterCodeList); + codeList.addAll(tagCodeList); + return aiDictService.mapNameByCodeList(codeList); + } + + @Override + public AiUserH5Output getAiUserH5Info(Long aiId) { + AiUser aiUser = getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, aiId)); + AiUserH5Output output = BeanConver.copeBean(aiUser, AiUserH5Output.class); + //角色,性格,标签 + Map aiDictNameMap = aiDictService.mapNameByCodeList(Sets.newHashSet(aiUser.getRoleCode(), aiUser.getCharacterCode(), aiUser.getTagCode())); + output.setCharacterName(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTagName(aiDictNameMap.get(aiUser.getTagCode())); + output.setRoleName(aiDictNameMap.get(aiUser.getRoleCode())); + //对话开场白 + AiUserExt aiUserExt = aiUserExtService.getAiUserExtByAiId(aiId); + output.setDialoguePrologue(aiUserExt != null ? aiUserExt.getDialoguePrologue() : ""); + //背景图 + output.setBackgroundImg(aiUser.getHomeImageUrl()); + return output; + } + + @Override + public AiUser getAiUserByAiId(Long aiId) { + return getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, aiId)); + } + + @Override + public AiUserImBaseInfoOutput getAiUserImBaseInfo(Long aiId, Long userId) { + //查询基础信息 + AiUser aiUser = getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, aiId)); + ToastResultCode.AI_USER_NOT_EXIST.check(aiUser == null); + //已经删除的,用户自己是不能看到的 + ToastResultCode.AI_USER_NOT_EXIST.check(aiUser.getIsDelete() && aiUser.getUserId().equals(userId)); + //已经删除的、私密的,只有有关系的人才能看到 + if (aiUser.getIsDelete() || (aiUser.getPermission() == 2 && !aiUser.getUserId().equals(userId))) { + //判断是否存在关系 + boolean isRelation = aiUserHeartbeatRelationService.relationCheck(userId, aiId); + ToastResultCode.AI_USER_NOT_EXIST.check(!isRelation); + } + AiUserImBaseInfoOutput output = BeanConver.copeBean(aiUser, AiUserImBaseInfoOutput.class); + //性格,标签名称,角色名称 + Map aiDictNameMap = aiDictService.mapNameByCodeList(Sets.newHashSet(aiUser.getRoleCode(), aiUser.getCharacterCode(), aiUser.getTagCode())); + output.setCharacterName(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTagName(aiDictNameMap.get(aiUser.getTagCode())); + output.setRoleName(aiDictNameMap.get(aiUser.getRoleCode())); + //被喜欢数 + output.setLikedNum(aiUserStatService.getAiLikedCount(aiId)); + //对话开场白放在扩展表中 + AiUserExt aiUserExt = aiUserExtService.getAiUserExtByAiId(aiId); + output.setDialoguePrologue(aiUserExt.getDialoguePrologue()); + //用户与AI的心动关系 + output.setAiUserHeartbeatRelation(aiUserHeartbeatRelationService.getAiUserHeartbeatRelation(userId, aiId)); + //语音类型,音高,语速 + TimbreDict timbreDict = timbreDictService.getTimbreDictByCode(aiUserExt.getDialogueTimbreCode()); + output.setVoiceType(timbreDict != null ? timbreDict.getVoiceType() : null); + output.setDialoguePitch(aiUserExt.getDialoguePitch()); + output.setDialogueSpeechRate(aiUserExt.getDialogueSpeechRate()); + //聊天设置相关 + ChatSet chatSet = chatSetService.getChatSet(userId, aiId); + //聊天背景-默认主图 + String chatBackgroundImg = aiUser.getHomeImageUrl(); + if (chatSet != null) { + //聊天设置相关-聊天背景 用户设置了用用户设置的背景,没有默认主图 + if (StringUtils.isNotEmpty(chatSet.getBackgroundImg()) && !chatSet.getBackgroundImg().equals(DEFAULT_BACKGROUND)) { + chatBackgroundImg = chatSet.getBackgroundImg(); + } + //聊天设置相关-是否自动播放语音 + output.setIsAutoPlayVoice(chatSet.getIsAutoPlayVoice()); + //聊天设置相关-是否删除聊天消息 + output.setIsDelChatted(chatSet.getIsDelChatted()); + } + //聊天气泡 + String userSetBubbleCode = chatSet != null ? chatSet.getBubbleCode() : null; + ChatBubbleDict chatBubbleDict = chatBubbleDictService.getUserChatBubble(userId, aiId, userSetBubbleCode); + ChatBubbleOutput chatBubbleOutput = BeanConvert.copeBean(chatBubbleDict, ChatBubbleOutput.class); + output.setChatBubble(chatBubbleOutput); + //背景图 + output.setBackgroundImg(chatBackgroundImg); + output.setIsDefaultBackground(chatBackgroundImg.equals(aiUser.getHomeImageUrl())); + //是不是会员类型 + output.setIsMember(subscribeClient.queryUserIsSubscribe(userId)); + //获取当前用户是否点赞过 + if (userId != null) { + output.setLiked(likedService.isLiked(userId, aiId, Liked.BizType.AI)); + } + //判断是否已聊天过 + AiUserHeartbeatRelation aiUserHeartbeatRelation = aiUserHeartbeatRelationService.getHeartbeatRelation(userId, aiId); + output.setIsHaveChatted(aiUserHeartbeatRelation != null); + //发送MQ,计算一次用户与AI的心动分 + commonSendMqService.calcHeartbeatScoreMq(userId, aiId); + return output; + } + + @Override + public Map mapByAiIdList(List aiIdList) { + if (CollectionUtils.isEmpty(aiIdList)) { + return Collections.emptyMap(); + } + //查询对应的AI用户信息 + List list = list(Wrappers.lambdaQuery() + .in(AiUser::getAiId, aiIdList) + ); + //获取字典数据 + Map aiDictNameMap = mapNameByCodeList(list); + List outputList = Lists.newArrayList(); + for (AiUser aiUser : list) { + AiUserBaseOutput output = BeanConvert.copeBean(aiUser, AiUserBaseOutput.class); + //组装字典数据 + output.setRoleName(aiDictNameMap.get(aiUser.getRoleCode())); + output.setCharacterName(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTagName(aiDictNameMap.get(aiUser.getTagCode())); + outputList.add(output); + } + return outputList.stream().collect(Collectors.toMap(AiUserBaseOutput::getAiId, output -> output)); + } + + @Override + public Integer countAiNumByUserId(Long userId) { + return count(Wrappers.lambdaQuery().eq(AiUser::getUserId, userId).eq(AiUser::getIsDelete, false)); + } + + @Override + public AiUserSeoBaseInfoOutput getAiUserSeoBaseInfo(Long aiId) { + AiUser aiUser = getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, aiId)); + AiUserSeoBaseInfoOutput output = new AiUserSeoBaseInfoOutput(); + output.setNickname(aiUser.getNickname()); + output.setHomeImageUrl(aiUser.getHomeImageUrl()); + return output; + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserServiceImpl.java new file mode 100644 index 0000000..7adcc98 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserServiceImpl.java @@ -0,0 +1,63 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Sets; +import com.sonic.frog.dao.AiUserDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.AiUserExt; +import com.sonic.frog.domain.entity.TimbreDict; +import com.sonic.frog.domain.output.AiInfoApiOutput; +import com.sonic.frog.service.AiDictService; +import com.sonic.frog.service.AiUserExtService; +import com.sonic.frog.service.AiUserService; +import com.sonic.frog.service.TimbreDictService; +import com.sonic.frog.utils.BeanConver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * AI用户表服务实现类 + */ +@Service +@Slf4j +public class AiUserServiceImpl extends ServiceImpl implements AiUserService { + + @Autowired + private AiUserExtService aiUserExtService; + @Autowired + private AiDictService aiDictService; + @Autowired + private TimbreDictService timbreDictService; + + + @Override + public AiInfoApiOutput getAiUserInfo(Long aiId) { + //获取ai用户信息 + AiUser aiUser = getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, aiId)); + //获取ai用户扩展信息 + AiUserExt aiUserExt = aiUserExtService.getOne(Wrappers.lambdaQuery().eq(AiUserExt::getAiId, aiId)); + //角色, 性格,标签名称 + AiInfoApiOutput output = BeanConver.copeBean(aiUser, AiInfoApiOutput.class); + Map aiDictNameMap = aiDictService.mapNameByCodeList(Sets.newHashSet(aiUser.getRoleCode(), aiUser.getCharacterCode(), aiUser.getTagCode())); + output.setUserId(aiUser.getUserId()); + output.setRoleName(aiDictNameMap.get(aiUser.getRoleCode())); + output.setCharacterName(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTagName(aiDictNameMap.get(aiUser.getTagCode())); + output.setProfile(aiUserExt.getProfile()); + output.setUserProfileExtJson(aiUserExt.getUserProfileExtJson()); + output.setDialogueStyle(aiUserExt.getDialogueStyle()); + output.setDialoguePrologue(aiUserExt.getDialoguePrologue()); + //基图 + output.setBaseImageUrl(aiUser.getBaseImageUrl()); + //语音类型,音高,语速 + TimbreDict timbreDict = timbreDictService.getTimbreDictByCode(aiUserExt.getDialogueTimbreCode()); + output.setVoiceType(timbreDict != null ? timbreDict.getVoiceType() : null); + output.setDialoguePitch(aiUserExt.getDialoguePitch()); + output.setDialogueSpeechRate(aiUserExt.getDialogueSpeechRate()); + return output; + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserSetServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserSetServiceImpl.java new file mode 100644 index 0000000..68066f8 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserSetServiceImpl.java @@ -0,0 +1,310 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.Sequence; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.sonic.bear.lib.client.UserNicknamePoolClient; +import com.sonic.bear.lib.client.UserSearchClient; +import com.sonic.bear.lib.enums.OptTypeEnum; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.cow.lib.client.ImageClient; +import com.sonic.cow.lib.client.NsfwCheckClient; +import com.sonic.frog.dao.AiUserDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.AiUserExt; +import com.sonic.frog.domain.input.CreateEditAiUserInput; +import com.sonic.frog.domain.input.EditAiHeadImgInput; +import com.sonic.frog.domain.output.AiUserExtOutput; +import com.sonic.frog.domain.output.AiUserInfoOutput; +import com.sonic.frog.domain.output.CreateEditAiUserOutput; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.event.inner.payload.AiImInfoPayload; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.BeanConver; +import com.sonic.frog.utils.RegexUtil; +import com.sonic.lion.lib.client.SubscribeClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Set; + +import static com.sonic.frog.enums.Constants.CREATE_AI_NO_LIMIT_USER_ID_LIST; + +/** + * AI用户表服务实现类 + */ +@Service +@Slf4j +public class AiUserSetServiceImpl extends ServiceImpl implements AiUserSetService { + @Autowired + private UserNicknamePoolClient userNicknamePoolClient; + @Autowired + private AiUserExtService aiUserExtService; + @Autowired + private AiDictService aiDictService; + @Autowired + private AiUserAlbumService aiUserAlbumService; + @Autowired + private TimbreDictService timbreDictService; + @Autowired + private ImageStyleDictService imageStyleDictService; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private AiUserStatService aiUserStatService; + @Autowired + private SubscribeClient subscribeClient; + + /** + * 构造ID生成器的对象 + */ + private final Sequence sequence = new Sequence(); + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private ImageClient imageClient; + @Autowired + private UserSearchClient userSearchClient; + @Autowired + private NsfwCheckClient nsfwCheckClient; + + @Transactional(rollbackFor = Exception.class) + @Override + public CreateEditAiUserOutput createEditAiUser(CreateEditAiUserInput input, Long currentUserId) { + //昵称是否修改 + boolean isNicknameUpdate = true; + if (input.getAiId() != null) { + AiUser aiUser = getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, input.getAiId()).eq(AiUser::getUserId, currentUserId)); + ToastResultCode.SYS_PERMISSION_DENIED.check(aiUser == null); + isNicknameUpdate = StringUtils.isNotEmpty(aiUser.getNickname()) && !aiUser.getNickname().equals(input.getNickname()); + } + if (isNicknameUpdate) { + //昵称重复性判断 + boolean nicknameExist = userNicknamePoolClient.userNicknameExistCheck(input.getNickname()); + ToastResultCode.USER_NICKNAME_EXIST.check(nicknameExist); + } + //昵称鉴黄 + nsfwCheckClient.checkContent(input.getNickname()); + //TODO 图片鉴黄 +// s3CheckClient.checkImage(input.getImageUrl()); + Long aiId = 0L; + AiUser aiUser = null; + AiImInfoPayload payload = new AiImInfoPayload(); + //是否同步相册默认图 + boolean isSyncAlbumDefault = false; + //判断添加的图片是否是ai生成的6张图之一 + imageClient.checkImageIsAIGenerated(currentUserId, Lists.newArrayList(input.getImageUrl())); + //开场白是否变化 + Boolean isDialoguePrologueChange = false; + //扩展表信息 + AiUserExt aiUserExt = null; + if (input.getAiId() == null) { + //创建作者AI虚拟角色限额不足时,提示 非会员1个,会员5个 + if (!CREATE_AI_NO_LIMIT_USER_ID_LIST.contains(currentUserId)) { + Boolean isMember = subscribeClient.queryUserIsSubscribe(currentUserId); + Integer aiNum = aiUserSearchService.countAiNumByUserId(currentUserId); + ToastResultCode.NOT_MEMBER_CREATE_AI_ONE.check((isMember == null || !isMember) && aiNum > 0); + ToastResultCode.MEMBER_CREATE_AI_MAX_NUM.check(isMember && aiNum >= 5); + } + //创建 + //aiId生成 + aiId = sequence.nextId(); + aiUser = BeanConver.copeBean(input, AiUser.class); + aiUser.setUserId(currentUserId); + //雪花算法生成 + aiUser.setAiId(aiId); + //AI虚拟角色的ID暂时不展示,使用创作者账户ID:251843333001XX,增加两位虚拟角色自增长ID即可 + aiUser.setIdCard(getAiUserIdCard(currentUserId)); + //设置基图,创建形象时设置,之后不会变 + aiUser.setBaseImageUrl(input.getImageUrl()); + aiUser.setCreateTime(LocalDateTime.now()); + aiUser.setEditorId(currentUserId); + aiUser.setEditTime(LocalDateTime.now()); + save(aiUser); + //扩展表保存 + aiUserExtService.saveOrUpdateAiUserExt(aiId, aiUserExt, input.getAiUserExt(), currentUserId); + //昵称同步 + userNicknamePoolClient.syncAiNickname(aiId, input.getNickname(), OptTypeEnum.ADD); + isSyncAlbumDefault = true; + payload.setAiId(aiId); + payload.setOptType(AiImInfoPayload.OptType.ADD); + //初始化Ai统计表 + aiUserStatService.initAiUserStat(aiId); + //开场白是否变化 + isDialoguePrologueChange = true; + } else { + //编辑 + //权限判断-只能自己编辑自己的 + aiUser = getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, input.getAiId()).eq(AiUser::getUserId, currentUserId)); + ToastResultCode.SYS_PERMISSION_DENIED.check(aiUser == null); + Long id = aiUser.getId(); + //aiId + aiId = aiUser.getAiId(); + //性别是不能变的 + Integer sex = aiUser.getSex(); + //形象图改了才同步 + if (StringUtils.isNotEmpty(aiUser.getImageUrl()) && !aiUser.getImageUrl().equals(input.getImageUrl())) { + isSyncAlbumDefault = true; + } + //对象复制更新 + aiUser = BeanConver.copeBean(input, AiUser.class); + aiUser.setSex(sex); + aiUser.setId(id); + updateById(aiUser); + //扩展表更新 + aiUserExt = aiUserExtService.getAiUserExtByAiId(aiId); + aiUserExtService.saveOrUpdateAiUserExt(aiUser.getAiId(), aiUserExt, input.getAiUserExt(), currentUserId); + //昵称同步 + userNicknamePoolClient.syncAiNickname(aiId, input.getNickname(), OptTypeEnum.UPD); + payload.setAiId(aiId); + payload.setOptType(AiImInfoPayload.OptType.UPDATE); + //开场白是否变化 + isDialoguePrologueChange = !aiUserExt.getDialoguePrologue().equals(input.getAiUserExt().getDialoguePrologue()) + ||!aiUserExt.getDialogueTimbreCode().equals(input.getAiUserExt().getDialogueTimbreCode()) + ||!aiUserExt.getDialogueSpeechRate().equals(input.getAiUserExt().getDialogueTimbreCode()) + ||!aiUserExt.getDialoguePitch().equals(input.getAiUserExt().getDialoguePitch()); + } + + //形象图片同步到相册且为默认图, + if (isSyncAlbumDefault) { + aiUserAlbumService.addDefaultAlbum(currentUserId, aiId, input.getImageUrl(), input.getImageWidth(), input.getImageHeight()); + } + + //发送MQ消息进行IM基础信息同步 + commonSendMqService.aiImInfoSendMq(payload); + //AI变化发送MQ消息 + commonSendMqService.aiChangeMq(aiId, isDialoguePrologueChange, input.getAiId() == null); + + return CreateEditAiUserOutput.builder() + .aiId(aiId) + .build(); + } + + /** + * AI虚拟角色的ID暂时不展示,使用创作者账户ID:251843333001XX,增加两位虚拟角色自增长ID即可 + * + * @param currentUserId + * @return + */ + private String getAiUserIdCard(Long currentUserId) { + // 先获取当前用户有没有创建过AI,并获取最后一个AI用户的idCard + AiUser aiUser = getOne(Wrappers.lambdaQuery() + .eq(AiUser::getUserId, currentUserId) + .orderByDesc(AiUser::getId) + .last("limit 1")); + + String idCard; + if (aiUser != null) { + String latestIdCard = aiUser.getIdCard(); + // 提取后两位数字并递增 + if (latestIdCard.length() >= 2 && latestIdCard.matches(".*\\d{2}")) { + String prefix = latestIdCard.substring(0, latestIdCard.length() - 2); + int number = Integer.parseInt(latestIdCard.substring(latestIdCard.length() - 2)); + idCard = prefix + String.format("%02d", number + 1); + } else { + // 如果没有符合格式的后两位数字,则追加"01" + idCard = latestIdCard + "01"; + } + } else { + //获取当前用户的idCard + BaseUserInfoOutput baseUserInfoOutput = userSearchClient.baseUserInfo(currentUserId); + String baseIdCard = baseUserInfoOutput.getIdCard(); + // 追加默认起始编号"01" + idCard = baseIdCard + "01"; + } + return idCard; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void delAiUser(Long aiId, Long currentUserId) { + //权限判断,非主人不能删除 + AiUser aiUser = getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, aiId).eq(AiUser::getUserId, currentUserId)); + ToastResultCode.SYS_PERMISSION_DENIED.check(aiUser == null); + //删除ai用户 + update(Wrappers.lambdaUpdate().eq(AiUser::getAiId, aiId).set(AiUser::getIsDelete, true)); + //删除ai用户扩展 + aiUserExtService.delAiUser(aiId); + //TODO 1、删除关联信息,这个需要仔细的对一下,看具体要删哪些信息 + + //昵称同步 + userNicknamePoolClient.syncAiNickname(aiId, null, OptTypeEnum.DEL); + + //AI变化发送MQ消息 + commonSendMqService.aiChangeMq(aiId, false, false); + } + + @Override + public AiUserInfoOutput getMyAiUserInfo(Long aiId, Long currentUserId) { + AiUser aiUser = getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, aiId).last("limit 1")); + if (aiUser == null) { + return new AiUserInfoOutput(); + } + //判断只有自己才能查看 + ToastResultCode.SYS_PERMISSION_DENIED.check(aiUser.getUserId() != null && !aiUser.getUserId().equals(currentUserId)); + //基础信息 + AiUserInfoOutput output = BeanConver.copeBean(aiUser, AiUserInfoOutput.class); + //扩展信息 + AiUserExt aiUserExt = aiUserExtService.getAiUserExtByAiId(aiId); + + //角色, 性格,标签名称 + Set codeList = Sets.newHashSet(aiUser.getRoleCode(), aiUser.getCharacterCode(), aiUser.getTagCode()); + if (aiUserExt != null && aiUserExt.getImageStyleCode() != null) { + codeList.add(aiUserExt.getImageStyleCode()); + } + Map aiDictNameMap = aiDictService.mapNameByCodeList(codeList); + output.setCharacter(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTag(aiDictNameMap.get(aiUser.getTagCode())); + output.setRole(aiDictNameMap.get(aiUser.getRoleCode())); + + //扩展信息 + if (aiUserExt != null) { + AiUserExtOutput aiUserExtOutput = BeanConver.copeBean(aiUserExt, AiUserExtOutput.class); + aiUserExtOutput.setTimbreDict(timbreDictService.getTimbreDictByCode(aiUserExt.getDialogueTimbreCode())); + aiUserExtOutput.setImageStyleDict(imageStyleDictService.getImageStyleDictByCode(aiUserExt.getImageStyleCode())); + output.setAiUserExt(aiUserExtOutput); + } + return output; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void setHomeImageUrl(Long aiId, String homeImageUrl) { + update(Wrappers.lambdaUpdate().set(AiUser::getHomeImageUrl, homeImageUrl).eq(AiUser::getAiId, aiId)); + } + + + @Override + public void editAiHeadImg(Long userId, EditAiHeadImgInput input) { + log.error("editAiHeadImg==UserInfoEditInput==> {}", JSONObject.toJSON(input)); + if (StringUtils.isNotEmpty(input.getUserHead())) { + Long aiId = input.getAiId(); + AiUser aiUser = aiUserSearchService.getAiUserByAiId(aiId); + ToastResultCode.AI_USER_NOT_EXIST.check(aiUser == null); + //权限判断,非主人不能修改 + ToastResultCode.SYS_PERMISSION_DENIED.check(aiUser.getUserId() != null && !aiUser.getUserId().equals(userId)); + //判断头像地址是不是符合要求的 + ToastResultCode.SYS_PARAMETERS_VALIDATE_EXCEPTION.check(!RegexUtil.isOssStartUrl(input.getUserHead())); + //判断头像是否有变更 + Boolean bl = ((StringUtils.isEmpty(aiUser.getHeadImg()) && !StringUtils.isEmpty(input.getUserHead())) + || (StringUtils.isNotEmpty(aiUser.getHeadImg()) && StringUtils.isNotEmpty(input.getUserHead()) && !aiUser.getHeadImg().equals(input.getUserHead()))); + if (bl) { + //更新数据库用户信息 + update(Wrappers.lambdaUpdate().set(AiUser::getHeadImg, input.getUserHead()).eq(AiUser::getAiId, aiId)); + //发送MQ消息进行IM基础信息同步 + AiImInfoPayload aiImInfoPayload = new AiImInfoPayload(); + aiImInfoPayload.setAiId(aiId); + aiImInfoPayload.setOptType(AiImInfoPayload.OptType.UPDATE); + commonSendMqService.aiImInfoSendMq(aiImInfoPayload); + } + } + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserStatServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserStatServiceImpl.java new file mode 100644 index 0000000..fefd761 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/AiUserStatServiceImpl.java @@ -0,0 +1,177 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.sonic.frog.dao.AiUserStatDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.AiUserStat; +import com.sonic.frog.domain.output.AiUserStatOutput; +import com.sonic.frog.enums.Constants; +import com.sonic.frog.event.inner.payload.AiUserStatPayload; +import com.sonic.frog.service.AiUserSearchService; +import com.sonic.frog.service.AiUserStatService; +import com.sonic.frog.utils.BeanConver; +import com.sonic.frog.utils.LimitUtils; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * AI用户统计业务实现类 + */ +@Service +@Slf4j +public class AiUserStatServiceImpl extends ServiceImpl implements AiUserStatService { + + @Autowired + private AiUserStatDao aiUserStatDao; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private AiUserSearchService aiUserSearchService; + + @Override + public AiUserStatOutput getAiUserStatByAiId(Long aiId, Long currentUserId) { + AiUserStat aiUserStat = getOne(Wrappers.lambdaQuery().eq(AiUserStat::getAiId, aiId).last("limit 1")); + AiUserStatOutput output = BeanConver.copeBean(aiUserStat, AiUserStatOutput.class); + //查看是否是当前用户的ai,如果不是则coinNum返回null + AiUser aiUser = aiUserSearchService.getAiUserByAiId(aiId); + if (currentUserId != null && aiUser != null && !aiUser.getUserId().equals(currentUserId)) { + output.setCoinNum(0L); + } + return output; + } + + @Override + public void initAiUserStat(Long aiId) { + AiUserStat aiUserStat = new AiUserStat(); + aiUserStat.setAiId(aiId); + aiUserStat.setLikedNum(0); + aiUserStat.setChatNum(0); + aiUserStat.setConversationNum(0); + aiUserStat.setCoinNum(0L); + aiUserStat.setGiftCoinNum(0L); + aiUserStat.setUnlockImgCoinNum(0L); + aiUserStat.setHeartbeatValTotal(new BigDecimal(0)); + aiUserStat.setCreateTime(LocalDateTime.now()); + aiUserStat.setEditTime(LocalDateTime.now()); + save(aiUserStat); + } + + @Override + public void aiUserStat(AiUserStatPayload payload) { + AiUserStatPayload.Type type = payload.getType(); + switch (type) { + case LIKED: + aiUserStatDao.updateLikedNum(payload.getAiId(), 1); + break; + case CANCEL_LIKED: + aiUserStatDao.updateLikedNum(payload.getAiId(), -1); + aiUserStatDao.updateDislikedNum(payload.getAiId(), 1); + break; + case CHAT: + increaseChatNum(payload.getAiId()); + break; + case FIRST_CHAT: + increaseConversationNum(payload.getAiId()); + break; + case GIFT: + //统计礼物打赏coin + increaseGiftCoinNum(payload.getAiId(), payload.getCoinNum()); + //统计总coin + increaseCoinNum(payload.getAiId(), payload.getCoinNum()); + break; + case UNLOCK_IMG: + //统计解锁图片coin + increaseUnlockImgCoinNum(payload.getAiId(), payload.getCoinNum()); + //统计总coin + increaseCoinNum(payload.getAiId(), payload.getCoinNum()); + break; + default: + } + } + + @Override + public void updateLikeNum(Long aiId, Integer num) { + aiUserStatDao.updateLikedNum(aiId, num); + } + + @Override + public void increaseChatNum(Long aiId) { + //聊天次数 redis累加,累加到20次时,更新一次 未达到聊天20次,定时任务每隔5分钟更新一次 + String aiChatNumKey = redisKeyUtils.aiChatNumKey(); + Double chatNum = stringRedisTemplate.opsForZSet().score(aiChatNumKey, aiId.toString()); + if (chatNum != null) { + stringRedisTemplate.opsForZSet().add(aiChatNumKey, aiId.toString(), chatNum + 1); + //达到20次更新到数据库 + if (chatNum + 1 == 20) { + //更新到数据库 + aiUserStatDao.increaseChatNum(aiId, 20); + // + stringRedisTemplate.opsForZSet().remove(aiChatNumKey, aiId.toString()); + } + } else { + stringRedisTemplate.opsForZSet().add(aiChatNumKey, aiId.toString(), 1); + } + } + + @Override + public void increaseChatNum(Long aiId, Integer num) { + //更新到数据库 + aiUserStatDao.increaseChatNum(aiId, num); + } + + @Override + public void increaseConversationNum(Long aiId) { + aiUserStatDao.increaseConversationNum(aiId); + } + + public void increaseGiftCoinNum(Long aiId, Long coinNum) { + aiUserStatDao.increaseGiftCoinNum(aiId, coinNum); + } + + public void increaseUnlockImgCoinNum(Long aiId, Long coinNum) { + aiUserStatDao.increaseUnlockImgCoinNum(aiId, coinNum); + } + + @Override + public void increaseCoinNum(Long aiId, Long coinNum) { + //统计用户实际coin + coinNum = new BigDecimal(coinNum).multiply(Constants.USER_RATE).longValue(); + aiUserStatDao.increaseCoinNum(aiId, coinNum); + } + + @Override + public Map queryAiLikedCount(List aiIds) { + if (CollectionUtils.isEmpty(aiIds)) { + return Collections.emptyMap(); + } + List list = list(Wrappers.lambdaQuery() + .select(AiUserStat::getAiId, AiUserStat::getLikedNum) + .in(AiUserStat::getAiId, aiIds) + ); + if (CollectionUtils.isNotEmpty(list)) { + return list.stream().collect(Collectors.toMap(AiUserStat::getAiId, AiUserStat::getLikedNum)); + } + return Collections.emptyMap(); + } + + @Override + public Integer getAiLikedCount(Long aiId) { + Map likedCountMap = queryAiLikedCount(Lists.newArrayList(aiId)); + return likedCountMap.get(aiId) != null ? likedCountMap.get(aiId) : 0; + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/BuyCreateCountRecordServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/BuyCreateCountRecordServiceImpl.java new file mode 100644 index 0000000..fbe9926 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/BuyCreateCountRecordServiceImpl.java @@ -0,0 +1,29 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.BuyCreateCountRecordDao; +import com.sonic.frog.domain.entity.BuyCreateCountRecord; +import com.sonic.frog.service.BuyCreateCountRecordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +/** + * 用户购买相册创作次数记录 Service实现类 + */ +@Slf4j +@Service +public class BuyCreateCountRecordServiceImpl extends ServiceImpl implements BuyCreateCountRecordService { + @Override + public void add(Long userId, Long totalAmount, Integer count, String orderNo) { + BuyCreateCountRecord buyCreateCountRecord = new BuyCreateCountRecord(); + buyCreateCountRecord.setUserId(userId); + buyCreateCountRecord.setBuyNum(count); + buyCreateCountRecord.setTotalAmount(totalAmount); + buyCreateCountRecord.setOrderNo(orderNo); + buyCreateCountRecord.setCreateTime(LocalDateTime.now()); + buyCreateCountRecord.setEditTime(LocalDateTime.now()); + save(buyCreateCountRecord); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/BuyHeartbeatValueRecordServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/BuyHeartbeatValueRecordServiceImpl.java new file mode 100644 index 0000000..054bd1f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/BuyHeartbeatValueRecordServiceImpl.java @@ -0,0 +1,20 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.BuyHeartbeatValueRecordDao; +import com.sonic.frog.domain.entity.BuyHeartbeatValueRecord; +import com.sonic.frog.service.BuyHeartbeatValueRecordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 用户购买心动值记录服务实现类 + */ +@Service +@Slf4j +public class BuyHeartbeatValueRecordServiceImpl extends ServiceImpl implements BuyHeartbeatValueRecordService { + @Override + public void addBuyHeartbeatValueRecord(BuyHeartbeatValueRecord buyHeartbeatValueRecord) { + save(buyHeartbeatValueRecord); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatBubbleDictServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatBubbleDictServiceImpl.java new file mode 100644 index 0000000..94298cb --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatBubbleDictServiceImpl.java @@ -0,0 +1,79 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.ChatBubbleDictDao; +import com.sonic.frog.domain.entity.ChatBubbleDict; +import com.sonic.frog.domain.output.ChatBubbleOutput; +import com.sonic.frog.enums.BubbleUnlockTypeEnum; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import com.sonic.frog.service.AiUserHeartbeatRelationService; +import com.sonic.frog.service.ChatBubbleDictService; +import com.sonic.frog.service.HeartbeatLevelDictService; +import com.sonic.frog.utils.BeanConvert; +import com.sonic.lion.lib.client.SubscribeClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 聊天气泡字典服务实现类 + */ +@Service +@Slf4j +public class ChatBubbleDictServiceImpl extends ServiceImpl implements ChatBubbleDictService { + + @Autowired + private SubscribeClient subscribeClient; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private HeartbeatLevelDictService heartbeatLevelDictService; + + @Override + public ChatBubbleDict getChatBubbleDictByCode(String chatBubbleCode) { + return getOne(Wrappers.lambdaQuery().eq(ChatBubbleDict::getCode, chatBubbleCode).last("limit 1")); + } + + @Override + public String getBubbleNameByCode(String bubbleCode) { + ChatBubbleDict chatBubbleDict = getChatBubbleDictByCode(bubbleCode); + return chatBubbleDict != null ? chatBubbleDict.getName() : null; + } + + @Override + public ChatBubbleDict getDefaultChatBubble() { + return getOne(Wrappers.lambdaQuery().eq(ChatBubbleDict::getIsDefault, true).last("limit 1")); + } + + @Override + public ChatBubbleDict getUserChatBubble(Long userId, Long aiId, String userSetBubbleCode) { + //用户设置了聊天气泡,根据聊天气泡解锁类型判断 + if (userSetBubbleCode != null) { + ChatBubbleDict chatBubbleDict = getChatBubbleDictByCode(userSetBubbleCode); + if (BubbleUnlockTypeEnum.MEMBER.equals(chatBubbleDict.getUnlockType())) { + //是否是会员 + Boolean isMember = subscribeClient.queryUserIsSubscribe(userId); + if (isMember) { + //会员才解锁 + return chatBubbleDict; + } + } else if (BubbleUnlockTypeEnum.HEARTBEAT_LEVEL.equals(chatBubbleDict.getUnlockType())) { + HeartbeatLevelEnum unlockHeartbeatLevel = chatBubbleDict.getUnlockHeartbeatLevel(); + //用户与AI的心动等级 + HeartbeatLevelEnum heartbeatLevelEnum = aiUserHeartbeatRelationService.getHeartbeatLevel(userId, aiId); + //解锁了哪些等级 + List unlockList = heartbeatLevelDictService.getUnlockListByCode(heartbeatLevelEnum); + //达到对应的心动等级才解锁 + if (unlockList.contains(unlockHeartbeatLevel)) { + return chatBubbleDict; + } + } else { + return chatBubbleDict; + } + } + return getDefaultChatBubble(); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatModelDictServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatModelDictServiceImpl.java new file mode 100644 index 0000000..e6f9127 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatModelDictServiceImpl.java @@ -0,0 +1,37 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.ChatModelDictDao; +import com.sonic.frog.domain.entity.ChatModelDict; +import com.sonic.frog.domain.output.ChatModelDictOutput; +import com.sonic.frog.service.ChatModelDictService; +import com.sonic.frog.utils.BeanConver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 对话模型字典服务实现类 + */ +@Service +@Slf4j +public class ChatModelDictServiceImpl extends ServiceImpl implements ChatModelDictService { + @Override + public List getChatModelDictList() { + List list = list(Wrappers.lambdaQuery().eq(ChatModelDict::getIsDelete, false).orderByDesc(ChatModelDict::getSort)); + return BeanConver.copeList(list, ChatModelDictOutput.class); + } + + @Override + public String getModelNameByCode(String modelCode) { + ChatModelDict chatModelDict = getOne(Wrappers.lambdaQuery().eq(ChatModelDict::getCode, modelCode).last("limit 1")); + return chatModelDict != null ? chatModelDict.getName() : null; + } + + @Override + public ChatModelDict getDefaultChatModel() { + return getOne(Wrappers.lambdaQuery().eq(ChatModelDict::getIsDefault, true).last("limit 1")); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatServiceImpl.java new file mode 100644 index 0000000..bc4ea6e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatServiceImpl.java @@ -0,0 +1,71 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.sonic.frog.domain.entity.AiUserExt; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.service.AiUserExtService; +import com.sonic.frog.service.ChatService; +import com.sonic.pigeon.lib.client.ImMessageClient; +import com.sonic.pigeon.lib.input.HistoryMessageInput; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import com.sonic.pigeon.lib.input.SendAiTextMessageInput; +import com.sonic.pigeon.lib.output.HistoryMessageOutput; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-08-29 17:52 + **/ +@Service +public class ChatServiceImpl implements ChatService { + + @Autowired + private ImMessageClient imMessageClient; + @Autowired + private AiUserExtService aiUserExtService; + + @Override + public void sendDialoguePrologueMessage(Long aiId, Long userId) { + AiUserExt aiUserExt = aiUserExtService.getAiUserExtByAiId(aiId); + ToastResultCode.SYS_PERMISSION_DENIED.check(aiUserExt == null); + //查看之前有没有发送过消息 + List historyMessageList = getHistoryMessageOutputs(aiId, userId); + //为空,没发送过才发送 + if (CollectionUtils.isEmpty(historyMessageList)) { + //发送开场白消息 + SendAiTextMessageInput sendAiTextMessageInput = SendAiTextMessageInput.builder() + .fromUserId(aiId) + .toUserId(userId) + .content(aiUserExt.getDialoguePrologue()) + .build(); + imMessageClient.sendAiToUserTextMessage(sendAiTextMessageInput); + } + } + + /** + * ai与用户是否已经聊天过 + * + * @param aiId + * @param userId + * @return + */ + private List getHistoryMessageOutputs(Long aiId, Long userId) { + HistoryMessageInput historyMessageInput = new HistoryMessageInput(); + historyMessageInput.setFromUserId(aiId); + historyMessageInput.setToUserId(userId); + historyMessageInput.setLimit(2); + historyMessageInput.setDescending(true); + List historyMessageList = imMessageClient.getAiToUserHistoryMessage(historyMessageInput); + return historyMessageList; + } + + @Override + public Boolean isHaveChatted(Long aiId, Long userId) { + List historyMessageOutputs = getHistoryMessageOutputs(aiId, userId); + return CollectionUtils.isNotEmpty(historyMessageOutputs) ? true : false; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatSetServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatSetServiceImpl.java new file mode 100644 index 0000000..097444a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatSetServiceImpl.java @@ -0,0 +1,272 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.sonic.bear.lib.input.EditUserInfoInput; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.cow.lib.client.NsfwCheckClient; +import com.sonic.frog.dao.ChatSetDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.ChatBubbleDict; +import com.sonic.frog.domain.entity.ChatModelDict; +import com.sonic.frog.domain.entity.ChatSet; +import com.sonic.frog.domain.input.*; +import com.sonic.frog.domain.output.ChatBubbleListOutput; +import com.sonic.frog.domain.output.ChatBubbleOutput; +import com.sonic.frog.domain.output.ChatSetOutput; +import com.sonic.frog.enums.BubbleUnlockTypeEnum; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.BeanConver; +import com.sonic.frog.utils.BeanConvert; +import com.sonic.lion.lib.client.SubscribeClient; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 用户与Ai的聊天设定表服务实现类 + */ +@Service +@Slf4j +public class ChatSetServiceImpl extends ServiceImpl implements ChatSetService { + + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private ChatModelDictService chatModelDictService; + @Autowired + private ChatBubbleDictService chatBubbleDictService; + @Autowired + private UserService userService; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private HeartbeatLevelDictService heartbeatLevelDictService; + @Autowired + private SubscribeClient subscribeClient; + @Autowired + private NsfwCheckClient nsfwCheckClient; + + @Override + public ChatSet getChatSet(Long userId, Long aiId) { + return getOne(Wrappers.lambdaQuery().eq(ChatSet::getUserId, userId).eq(ChatSet::getAiId, aiId).last("limit 1")); + } + + @Override + public ChatSetOutput getMyChatSet(Long userId, Long aiId) { + //获取用户与Ai的聊天设定 + ChatSet chatSet = getChatSet(userId, aiId); + //获取ai的信息 + AiUser aiUser = aiUserSearchService.getAiUserByAiId(aiId); + ToastResultCode.AI_USER_NOT_EXIST.check(aiUser == null); + //获取当前用户基础信息 + BaseUserInfoOutput baseUserInfo = userService.baseUserInfo(userId); + //构建输出 + ChatSetOutput output = new ChatSetOutput(); + output.setAiId(aiId); + //人物设定 昵称,年龄,性别 + output.setNickname(baseUserInfo.getNickname()); + output.setSex(baseUserInfo.getSex()); + output.setBirthday(baseUserInfo.getBirthday()); + //聊天背景,默认使用ai的主图 + output.setBackgroundImg(aiUser.getHomeImageUrl()); + output.setIsDefaultBackground(true); + //获取默认对话模型 + ChatModelDict defaultChatModel = chatModelDictService.getDefaultChatModel(); + output.setModelCode(defaultChatModel.getCode()); + output.setModelName(defaultChatModel.getName()); + //自动播放语音,默认关闭 + output.setIsAutoPlayVoice(0); + if (chatSet != null) { + //昵称,生日 如果设置了用设置的 + if (StringUtils.isNotEmpty(chatSet.getNickname())) { + output.setNickname(chatSet.getNickname()); + } + if (chatSet.getBirthday() != null) { + output.setBirthday(chatSet.getBirthday()); + } + //获取我是谁 + output.setWhoAmI(chatSet.getWhoAmI()); + //获取用户设置的对话模型名称 + if (StringUtils.isNotEmpty(chatSet.getModelCode())) { + output.setModelCode(chatSet.getModelCode()); + output.setModelName(chatModelDictService.getModelNameByCode(chatSet.getModelCode())); + } + //聊天背景图片 + if (StringUtils.isNotEmpty(chatSet.getBackgroundImg())) { + output.setBackgroundImg(chatSet.getBackgroundImg()); + output.setIsDefaultBackground(chatSet.getBackgroundImg().equals(aiUser.getHomeImageUrl())); + } + //是否自动播放 + output.setIsAutoPlayVoice(chatSet.getIsAutoPlayVoice() != null ? chatSet.getIsAutoPlayVoice() : 0); + } + //聊天气泡 + String userSetBubbleCode = chatSet != null ? chatSet.getBubbleCode() : null; + ChatBubbleDict chatBubbleDict = chatBubbleDictService.getUserChatBubble(userId, aiId, userSetBubbleCode); + output.setBubbleCode(chatBubbleDict.getCode()); + output.setBubbleName(chatBubbleDict.getName()); + return output; + } + + + /** + * 初始化或获取用户与Ai的聊天设定 + * + * @param userId + * @param aiId + * @return + */ + private ChatSet initAndGetChatSet(Long userId, Long aiId) { + ChatSet chatSet = getOne(Wrappers.lambdaQuery().eq(ChatSet::getUserId, userId).eq(ChatSet::getAiId, aiId).last("limit 1")); + if (chatSet == null) { + chatSet = new ChatSet(); + chatSet.setUserId(userId); + chatSet.setAiId(aiId); + save(chatSet); + } + return chatSet; + } + + @Override + public void setMyChatSetting(Long userId, SetMyChatSettingInput input) { + EditUserInfoInput editUserInfoInput = new EditUserInfoInput(); + editUserInfoInput.setUserId(userId); + editUserInfoInput.setNickname(input.getNickname()); + editUserInfoInput.setBirthday(input.getBirthday()); + //修改用户基础信息 + userService.editUserInfo(editUserInfoInput); + + //初始化或获取用户与Ai的聊天设定 + ChatSet chatSet = initAndGetChatSet(userId, input.getAiId()); + //更新 + update(Wrappers.lambdaUpdate() + .set(ChatSet::getNickname, input.getNickname()) + .set(ChatSet::getBirthday, input.getBirthday()) + .set(ChatSet::getWhoAmI, input.getWhoAmI()) + .eq(ChatSet::getId, chatSet.getId())); + } + + @Override + public void setChatModel(Long userId, SetChatModelInput input) { + //初始化或获取用户与Ai的聊天设定 + ChatSet chatSet = initAndGetChatSet(userId, input.getAiId()); + //更新聊天对话模型 + update(Wrappers.lambdaUpdate() + .set(ChatSet::getModelCode, input.getCode()) + .eq(ChatSet::getId, chatSet.getId())); + } + + @Override + public void setChatBubble(Long userId, SetChatBubbleInput input) { + String code = input.getCode(); + ChatBubbleDict chatBubbleDict = chatBubbleDictService.getChatBubbleDictByCode(code); + ToastResultCode.CHAT_BUBBLE_NOT_EXIST.check(chatBubbleDict == null); + BubbleUnlockTypeEnum unlockType = chatBubbleDict.getUnlockType(); + //会员解锁时,判断是否是会员 + if (BubbleUnlockTypeEnum.MEMBER.equals(unlockType)) { + Boolean isMember = subscribeClient.queryUserIsSubscribe(userId); + ToastResultCode.SYS_PERMISSION_DENIED.check(isMember == null || !isMember); + } + //心动等级解锁时,判断等级是否达到 + if (BubbleUnlockTypeEnum.HEARTBEAT_LEVEL.equals(unlockType)) { + HeartbeatLevelEnum heartbeatLevelEnum = aiUserHeartbeatRelationService.getHeartbeatLevel(userId, input.getAiId()); + List unlockHeartbeatLevelList = heartbeatLevelDictService.getUnlockListByCode(heartbeatLevelEnum); + ToastResultCode.SYS_PERMISSION_DENIED.check(heartbeatLevelEnum == null || (!unlockHeartbeatLevelList.contains(heartbeatLevelEnum))); + } + //初始化或获取用户与Ai的聊天设定 + ChatSet chatSet = initAndGetChatSet(userId, input.getAiId()); + //更新聊天气泡 + update(Wrappers.lambdaUpdate() + .set(ChatSet::getBubbleCode, input.getCode()) + .eq(ChatSet::getId, chatSet.getId())); + } + + @Override + public void setBackground(Long userId, Long aiId, String imgUrl) { + //初始化或获取用户与Ai的聊天设定 + ChatSet chatSet = initAndGetChatSet(userId, aiId); + //更新聊天背景图 + update(Wrappers.lambdaUpdate() + .set(ChatSet::getBackgroundImg, imgUrl) + .eq(ChatSet::getId, chatSet.getId())); + } + + @Override + public void setIsAutoPlayVoice(Long userId, SetIsAutoPlayVoiceInput input) { + //判断是否会员 + Boolean isMember = subscribeClient.queryUserIsSubscribe(userId); + ToastResultCode.SYS_PERMISSION_DENIED.check(isMember == null || !isMember); + //初始化或获取用户与Ai的聊天设定 + ChatSet chatSet = initAndGetChatSet(userId, input.getAiId()); + //更新聊天背景图 + update(Wrappers.lambdaUpdate() + .set(ChatSet::getIsAutoPlayVoice, input.getIsAutoPlayVoice()) + .eq(ChatSet::getId, chatSet.getId())); + } + + @Override + public List getChatBubbleList(Long userId, ChatBubbleListInput input) { + //字典列表 + List list = chatBubbleDictService.list(Wrappers.lambdaQuery() + .eq(ChatBubbleDict::getIsDelete, false) + .orderByDesc(ChatBubbleDict::getIsDefault) + .orderByDesc(ChatBubbleDict::getSort) + ); + //是否是会员 + Boolean isMember = subscribeClient.queryUserIsSubscribe(userId); + //用户与AI的心动等级 + HeartbeatLevelEnum heartbeatLevelEnum = aiUserHeartbeatRelationService.getHeartbeatLevel(userId, input.getAiId()); + //解锁了哪些等级 + List unlockList = heartbeatLevelDictService.getUnlockListByCode(heartbeatLevelEnum); + //输出 + List outputList = Lists.newArrayList(); + list.forEach(chatBubbleDict -> { + ChatBubbleListOutput output = BeanConver.copeBean(chatBubbleDict, ChatBubbleListOutput.class); + //会员解锁时,判断是否是会员 + if (BubbleUnlockTypeEnum.MEMBER.equals(chatBubbleDict.getUnlockType())) { + output.setIsUnlock(isMember != null && isMember); + } else if (BubbleUnlockTypeEnum.HEARTBEAT_LEVEL.equals(chatBubbleDict.getUnlockType())) { + //心动等级解锁时,判断等级是否达到 + output.setIsUnlock(heartbeatLevelEnum != null && unlockList.contains(chatBubbleDict.getUnlockHeartbeatLevel())); + } else { + output.setIsUnlock(true); + } + outputList.add(output); + }); + return outputList; + } + + @Override + public String getChatSetBackground(Long userId, Long aiId) { + ChatSet chatSet = getChatSet(userId, aiId); + return chatSet != null ? chatSet.getBackgroundImg() : null; + } + + @Override + public void updateIsDelChatted(Long userId, List aiIdList) { + if (CollectionUtils.isEmpty(aiIdList)) { + return; + } + update(Wrappers.lambdaUpdate() + .eq(ChatSet::getUserId, userId) + .in(ChatSet::getAiId, aiIdList) + .set(ChatSet::getIsDelChatted, 1)); + } + + @Override + public void closeAutoPlayVoice(List userIdList) { + update(Wrappers.lambdaUpdate() + .set(ChatSet::getIsAutoPlayVoice, false) + .in(ChatSet::getUserId, userIdList) + .eq(ChatSet::getIsAutoPlayVoice, true)); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatUserBackgroundServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatUserBackgroundServiceImpl.java new file mode 100644 index 0000000..c52cbf8 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ChatUserBackgroundServiceImpl.java @@ -0,0 +1,151 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.sonic.cow.lib.client.ImageClient; +import com.sonic.frog.dao.ChatUserBackgroundDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.ChatUserBackground; +import com.sonic.frog.domain.input.AddBackgroundInput; +import com.sonic.frog.domain.input.AiAlbumImageInfo; +import com.sonic.frog.domain.input.BatchAddBackgroundInput; +import com.sonic.frog.domain.input.SetBackgroundInput; +import com.sonic.frog.domain.output.BackgroundImgListOutput; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.RegexUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import static com.sonic.frog.enums.Constants.DEFAULT_BACKGROUND; + +/** + * 对话用户聊天背景服务实现类 + */ +@Service +@Slf4j +public class ChatUserBackgroundServiceImpl extends ServiceImpl implements ChatUserBackgroundService { + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private ChatSetService chatSetService; + @Autowired + private ImageClient imageClient; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private HeartbeatLevelDictService heartbeatLevelDictService; + + @Override + public List batchAddBackground(Long currentUserId, BatchAddBackgroundInput input) { + List images = input.getImages().stream().filter(info -> RegexUtil.isOssStartUrl(info.getUrl())).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(images)) { + return null; + } + //最多添加一张 + ToastResultCode.BACKGROUND_ADD_MAX_ONE.check(images.size() > 1); + //判断添加的图片是否是ai生成的6张图之一 + imageClient.checkImageIsAIGenerated(currentUserId, input.getImages().stream().map(AddBackgroundInput::getUrl).collect(Collectors.toList())); + //权限检验,只有主人才能修改 + AiUser aiUser = aiUserSearchService.getAiUserByAiId(input.getAiId()); + ToastResultCode.SYS_PERMISSION_DENIED.check(aiUser == null); + //等级10才能添加 + HeartbeatLevelEnum heartbeatLevel = aiUserHeartbeatRelationService.getHeartbeatLevel(currentUserId, input.getAiId()); + List unlockHearbeatLevelList = heartbeatLevelDictService.getUnlockListByCode(heartbeatLevel); + ToastResultCode.SYS_PERMISSION_DENIED.check(!unlockHearbeatLevelList.contains(HeartbeatLevelEnum.LEVEL_10)); + //判断添加的图片是否是ai生成的6张图 + imageClient.checkImageIsAIGenerated(currentUserId, images.stream().map(AddBackgroundInput::getUrl).collect(Collectors.toList())); + //TODO 校验提交的图片地址是不是符合规范的,是否已经鉴黄通过 +// s3CheckClient.checkImage(images.stream().map(AiAlbumImageInfo::getUrl).collect(Collectors.toList())); + List idList = Lists.newArrayList(); + for (AddBackgroundInput imageInfo : images) { + ChatUserBackground chatUserBackground = ChatUserBackground.builder() + .imgUrl(imageInfo.getUrl()) + .width(imageInfo.getWidth()) + .height(imageInfo.getHeight()) + .aiId(input.getAiId()) + .userId(currentUserId) + .isDelete(false) + .createTime(LocalDateTime.now()) + .editTime(LocalDateTime.now()) + .build(); + save(chatUserBackground); + idList.add(chatUserBackground.getId()); + } + return idList; + } + + @Override + public List getBackgroundImgList(Long userId, Long aiId) { + List list = list(Wrappers.lambdaQuery() + .eq(ChatUserBackground::getUserId, userId) + .eq(ChatUserBackground::getAiId, aiId) + .eq(ChatUserBackground::getIsDelete, false) + .orderByDesc(ChatUserBackground::getId) + ); + List outputList = Lists.newArrayList(); + //获取用户设备的背景 + String chatSetBackgroundImg = chatSetService.getChatSetBackground(userId, aiId); + //默认背景图片 添加第一个 + AiUser aiUser = aiUserSearchService.getAiUserByAiId(aiId); + if (aiUser != null && StringUtils.isNotEmpty(aiUser.getHomeImageUrl())) { + Boolean isSelected = chatSetBackgroundImg == null || aiUser.getHomeImageUrl().equals(chatSetBackgroundImg); + outputList.add(BackgroundImgListOutput.builder() + .backgroundId(null) + .imgUrl(aiUser.getHomeImageUrl()) + .width("720") + .height("1024") + .isDefault(true) + .isSelected(isSelected) + .build()); + } + list.forEach(background -> { + outputList.add(BackgroundImgListOutput.builder() + .backgroundId(background.getId()) + .imgUrl(background.getImgUrl()) + .width(background.getWidth()) + .height(background.getHeight()) + .isDefault(false) + .isSelected(chatSetBackgroundImg != null && chatSetBackgroundImg.equals(background.getImgUrl())) + .build()); + }); + return outputList; + } + + @Override + public void setBackground(Long currentUserId, SetBackgroundInput input) { + //默认背景图片 + if (input.getBackgroundId() == null) { + chatSetService.setBackground(currentUserId, input.getAiId(), DEFAULT_BACKGROUND); + return; + } + //权限检验,只有主人才能修改 + ChatUserBackground chatUserBackground = getOne(Wrappers.lambdaQuery().eq(ChatUserBackground::getId, input.getBackgroundId())); + ToastResultCode.SYS_PERMISSION_DENIED.check(chatUserBackground == null); + ToastResultCode.SYS_PERMISSION_DENIED.check(!chatUserBackground.getUserId().equals(currentUserId)); + //设置图片为用户与AI的聊天背景 + chatSetService.setBackground(currentUserId, input.getAiId(), chatUserBackground.getImgUrl()); + } + + @Override + public void delBackground(Long currentUserId, Long backgroundId) { + //权限检验,只有主人才能修改 + ChatUserBackground chatUserBackground = getOne(Wrappers.lambdaQuery().eq(ChatUserBackground::getId, backgroundId)); + ToastResultCode.BACKGROUND_NOT_EXIST.check(chatUserBackground == null); + ToastResultCode.SYS_PERMISSION_DENIED.check(!chatUserBackground.getUserId().equals(currentUserId)); + ToastResultCode.BACKGROUND_IS_DELETED.check(chatUserBackground.getIsDelete()); + //软删除 + update(Wrappers.lambdaUpdate().set(ChatUserBackground::getIsDelete, true).eq(ChatUserBackground::getId, backgroundId)); + //如果删除的是设置背景,需要默认为默认背景 + chatSetService.setBackground(currentUserId, chatUserBackground.getAiId(), null); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/CommonMessageServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/CommonMessageServiceImpl.java new file mode 100644 index 0000000..7f23971 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/CommonMessageServiceImpl.java @@ -0,0 +1,71 @@ +package com.sonic.frog.service.impl; + + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.Maps; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.enums.Constants; +import com.sonic.frog.service.AiUserSearchService; +import com.sonic.frog.service.CommonMessageService; +import com.sonic.pigeon.lib.client.MessageClient; +import com.sonic.pigeon.lib.enums.StationMessageTypeEnum; +import com.sonic.pigeon.lib.input.SendMessageInput; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Map; + +/** + * 公共发送系统通知 + */ +@Service +public class CommonMessageServiceImpl implements CommonMessageService { + + @Autowired + private MessageClient messageClient; + @Autowired + private AiUserSearchService aiUserSearchService; + + @Override + public void aiGiftSendMessage(Long userId, Long aiId, String giftName, Integer giftNum, Long totalAmount) { + AiUser aiUser = aiUserSearchService.getAiUserByAiId(aiId); + String title = StationMessageTypeEnum.AI_GIFT.getTitle(); + //用户实际所得金额 + BigDecimal userAmount = new BigDecimal(totalAmount).multiply(Constants.USER_RATE).divide(new BigDecimal(100), 0, RoundingMode.CEILING); + //用户实际所得金额大于0才发送 + if (userAmount.compareTo(BigDecimal.ZERO) > 0) { + String content = String.format(StationMessageTypeEnum.AI_GIFT.getContent(), aiUser.getNickname(), giftName, giftNum, userAmount); + SendMessageInput sendMessageInput = new SendMessageInput(userId, aiUser.getUserId(), StationMessageTypeEnum.AI_GIFT.getIndex(), title, content); + messageClient.sendMessage(sendMessageInput); + } + } + + @Override + public void aiHeartbeatLevelDowngradeSendMessage(Long userId, Long aiId, String heartbeatLevelName) { + AiUser aiUser = aiUserSearchService.getAiUserByAiId(aiId); + String title = StationMessageTypeEnum.HEARTBEAT_LEVEL_DOWN.getTitle(); + String content = String.format(StationMessageTypeEnum.HEARTBEAT_LEVEL_DOWN.getContent(), aiUser.getNickname(), heartbeatLevelName); + //额外扩展字段 + Map extra = Maps.newHashMap(); + extra.put("aiId", aiId); + SendMessageInput sendMessageInput = new SendMessageInput(-1L, userId, StationMessageTypeEnum.HEARTBEAT_LEVEL_DOWN.getIndex(), title, content); + sendMessageInput.setExtras(JSON.toJSONString(extra)); + messageClient.sendMessage(sendMessageInput); + } + + @Override + public void unlockAlbumImgSendMessage(Long userId, Long aiId, Long albumId, Long unlockAmount) { + AiUser aiUser = aiUserSearchService.getAiUserByAiId(aiId); + String title = StationMessageTypeEnum.AI_IMG_UNLOCK.getTitle(); + //用户实际所得金额,除以100 + BigDecimal userAmount = new BigDecimal(unlockAmount).multiply(Constants.USER_RATE).divide(new BigDecimal(100), 0, RoundingMode.CEILING); + //用户实际所得金额大于0才发送 + if (userAmount.compareTo(BigDecimal.ZERO) > 0) { + String content = String.format(StationMessageTypeEnum.AI_IMG_UNLOCK.getContent(), aiUser.getNickname(), userAmount); + SendMessageInput sendMessageInput = new SendMessageInput(userId, aiUser.getUserId(), StationMessageTypeEnum.AI_IMG_UNLOCK.getIndex(), title, content); + messageClient.sendMessage(sendMessageInput); + } + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/CommonSendMqServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/CommonSendMqServiceImpl.java new file mode 100644 index 0000000..4422e2c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/CommonSendMqServiceImpl.java @@ -0,0 +1,173 @@ +package com.sonic.frog.service.impl; + + +import com.sonic.common.event.Event; +import com.sonic.common.event.EventProducer; +import com.sonic.common.event.RabbitmqEventProducer; +import com.sonic.frog.enums.MessageTypeEnum; +import com.sonic.frog.event.inner.payload.*; +import com.sonic.frog.event.outer.payload.AiChatPayload; +import com.sonic.frog.service.CommonSendMqService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +import static com.sonic.frog.enums.Constants.threshold; +import static com.sonic.frog.event.inner.EventType.*; +import static com.sonic.frog.event.outer.EventType.AI_CHAT; + +/** + * 发送消息到im的mq + */ +@Service +@Slf4j +public class CommonSendMqServiceImpl implements CommonSendMqService { + + @Autowired + private EventProducer eventProducer; + + @Autowired + @Qualifier("aiImInfoMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta aiImInfoMeta; + @Autowired + @Qualifier("calcHeartbeatLevelMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta calcHeartbeatLevelMeta; + @Autowired + @Qualifier("aiChatMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta aiChatMeta; + @Autowired + @Qualifier("subtractHeartbeatValMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta subtractHeartbeatValMeta; + @Autowired + @Qualifier("calcHeartbeatScoreMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta calcHeartbeatScoreMeta; + @Autowired + @Qualifier("calcHeartbeatRankMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta calcHeartbeatRankMeta; + @Autowired + @Qualifier("aiUserStatMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta aiUserStatMeta; + @Autowired + @Qualifier("aiChangeMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta aiChangeMeta; + + + @Override + public void aiImInfoSendMq(AiImInfoPayload payload) { + eventProducer.send(Event.builder() + .eventScene(AI_IM_INFO.getEventCode().getScene()) + .eventModule(AI_IM_INFO.getEventCode().getModule()) + .eventName(AI_IM_INFO.getEventCode().getName()) + .data(payload).build(), aiImInfoMeta); + } + + @Override + public void calHeartbeatLevelSendMq(CalcHeartbeatLevelPayload payload) { + eventProducer.send(Event.builder() + .eventScene(CALC_HEARTBEAT_LEVEL.getEventCode().getScene()) + .eventModule(CALC_HEARTBEAT_LEVEL.getEventCode().getModule()) + .eventName(CALC_HEARTBEAT_LEVEL.getEventCode().getName()) + .data(payload).build(), calcHeartbeatLevelMeta); + } + + @Override + public void sendAiChatMq(Long fromUserId, Long toUserId, String content, MessageTypeEnum messageType, String attach, AiChatPayload.SourceType sourceType) { + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(AI_CHAT.getEventCode().getScene()) + .eventModule(AI_CHAT.getEventCode().getModule()) + .eventName(AI_CHAT.getEventCode().getName()) + .data(AiChatPayload.builder() + .fromUserId(fromUserId) + .toUserId(toUserId) + .messageType(messageType.name()) + .content(content) + .attach(attach) + .sourceType(sourceType) + .build()).build(), aiChatMeta); + } + + @Override + public void calcHeartbeatScoreMq(Long userId, Long aiId) { + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(CALC_HEARTBEAT_SCORE.getEventCode().getScene()) + .eventModule(CALC_HEARTBEAT_SCORE.getEventCode().getModule()) + .eventName(CALC_HEARTBEAT_SCORE.getEventCode().getName()) + .data(CalcHeartbeatScorePayload.builder() + .userId(userId) + .aiId(aiId) + .build()).build(), calcHeartbeatScoreMeta); + } + + @Override + public void calcHeartbeatRankMq(Long aiId, Long userId, BigDecimal oldHeartbeatVal, BigDecimal newHeartbeatVal) { + //判断,如果前后的分值都小于15分的话,那么不发送mq消息 + if(oldHeartbeatVal.compareTo(threshold) < 0 && newHeartbeatVal.compareTo(threshold) < 0) { + return; + } + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(CALC_HEARTBEAT_RANK.getEventCode().getScene()) + .eventModule(CALC_HEARTBEAT_RANK.getEventCode().getModule()) + .eventName(CALC_HEARTBEAT_RANK.getEventCode().getName()) + .data(CalcAiUserHeartbeatRankPayload.builder() + .userId(userId) + .aiId(aiId) + .oldHeartbeatVal(oldHeartbeatVal) + .newHeartbeatVal(newHeartbeatVal) + .build()).build(), calcHeartbeatRankMeta); + } + + public static void main(String[] args) { + BigDecimal threshold = new BigDecimal("12"); + BigDecimal oldHeartbeatVal = new BigDecimal(12); + BigDecimal newHeartbeatVal = new BigDecimal(0); + System.out.println(oldHeartbeatVal.compareTo(threshold)); + System.out.println(oldHeartbeatVal.compareTo(threshold) == 0); + System.out.println(newHeartbeatVal.compareTo(threshold) == 0); + + } + + @Override + public void subtractHeartbeatValMq(Long userId) { + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(SUBTRACT_HEARTBEAT_VAL.getEventCode().getScene()) + .eventModule(SUBTRACT_HEARTBEAT_VAL.getEventCode().getModule()) + .eventName(SUBTRACT_HEARTBEAT_VAL.getEventCode().getName()) + .data(SubtractHeartbeatValPayload.builder() + .userId(userId) + .build()).build(), subtractHeartbeatValMeta); + } + + @Override + public void aiUserStatMq(Long aiId, AiUserStatPayload.Type type, Long coinNum) { + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(AI_USER_STAT.getEventCode().getScene()) + .eventModule(AI_USER_STAT.getEventCode().getModule()) + .eventName(AI_USER_STAT.getEventCode().getName()) + .data(AiUserStatPayload.builder() + .aiId(aiId) + .type(type) + .coinNum(coinNum) + .build()).build(), aiUserStatMeta); + } + @Override + public void aiChangeMq(Long aiId, Boolean isDialoguePrologueChange, Boolean isCreate) { + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(AI_CHANGE.getEventCode().getScene()) + .eventModule(AI_CHANGE.getEventCode().getModule()) + .eventName(AI_CHANGE.getEventCode().getName()) + .data(AiChangePayload.builder() + .aiId(aiId) + .isDialoguePrologueChange(isDialoguePrologueChange) + .isCreate(isCreate) + .build()).build(), aiChangeMeta); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ExploreServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ExploreServiceImpl.java new file mode 100644 index 0000000..f46ace6 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ExploreServiceImpl.java @@ -0,0 +1,29 @@ +package com.sonic.frog.service.impl; + +import com.sonic.frog.domain.output.ExploreInfoOutput; +import com.sonic.frog.service.AdvertiseService; +import com.sonic.frog.service.ExploreService; +import com.sonic.frog.service.RankService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class ExploreServiceImpl implements ExploreService { + + @Autowired + private AdvertiseService advertiseService; + @Autowired + private RankService rankService; + + @Override + public ExploreInfoOutput exploreInfo(Long userId) { + ExploreInfoOutput output = new ExploreInfoOutput(); + output.setAdvertiseList(advertiseService.getAdvertiseList(userId)); + output.setAiChatRankTop3List(rankService.chatRank(userId, 3)); + output.setAiHeartbeatRankTop3List(rankService.aiHeartbeatRank(userId, 3)); + output.setAiGiftRankTop3List(rankService.giftRank(userId, 3)); + return output; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/GiftDictServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/GiftDictServiceImpl.java new file mode 100644 index 0000000..a5c5536 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/GiftDictServiceImpl.java @@ -0,0 +1,60 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.common.rpc.Page; +import com.sonic.daosupport.converter.PageConverter; +import com.sonic.frog.dao.GiftDictDao; +import com.sonic.frog.domain.entity.GiftDict; +import com.sonic.frog.domain.entity.HeartbeatLevelDict; +import com.sonic.frog.domain.input.GiftDictListInput; +import com.sonic.frog.domain.output.GiftDictListOutput; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import com.sonic.frog.service.GiftDictService; +import com.sonic.frog.service.HeartbeatLevelDictService; +import com.sonic.frog.utils.BeanConver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 礼物字典业务实现类 + */ +@Service +@Slf4j +public class GiftDictServiceImpl extends ServiceImpl implements GiftDictService { + + @Autowired + private HeartbeatLevelDictService heartbeatLevelDictService; + + @Override + public Page getGiftDictList(GiftDictListInput input) { + //分页获取 + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(input.getPage().getPn(), input.getPage().getPs()); + IPage giftDictPage = page(page, Wrappers.lambdaQuery() + .eq(GiftDict::getIsDelete, false) + .orderByAsc(GiftDict::getPrice,GiftDict::getSort) + ); + List records = page.getRecords(); + //心动等级map + Set heartbeatLevelEnumSet = records.stream().filter(e -> e.getHeartbeatLevel() != null).map(GiftDict::getHeartbeatLevel).collect(Collectors.toSet()); + Map heartbeatLevelDictMap = heartbeatLevelDictService.mapByLevel(heartbeatLevelEnumSet); + return PageConverter.convert(giftDictPage, giftDict -> { + GiftDictListOutput output = BeanConver.copeBean(giftDict, GiftDictListOutput.class); + //心动等级 + HeartbeatLevelEnum heartbeatLevel = giftDict.getHeartbeatLevel(); + if (heartbeatLevel != null) { + HeartbeatLevelDict heartbeatLevelDict = heartbeatLevelDictMap.get(heartbeatLevel); + //心动等级开始值 + output.setStartVal(heartbeatLevelDict != null ? heartbeatLevelDict.getStartVal() : null); + } + return output; + }); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/GiftRewardRecordServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/GiftRewardRecordServiceImpl.java new file mode 100644 index 0000000..d3b54e0 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/GiftRewardRecordServiceImpl.java @@ -0,0 +1,41 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.GiftRewardRecordDao; +import com.sonic.frog.domain.entity.GiftDict; +import com.sonic.frog.domain.entity.GiftRewardRecord; +import com.sonic.frog.enums.Constants; +import com.sonic.frog.service.GiftRewardRecordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +/** + * 礼物打赏记录服务实现类 + */ +@Service +@Slf4j +public class GiftRewardRecordServiceImpl extends ServiceImpl implements GiftRewardRecordService { + @Override + public void addGiftRewardRecord(GiftDict giftDict, String orderNo, Long fromUid, Long toUid, Integer num) { + Long total = giftDict.getPrice() * num; + // 用户实际收入 + long incomeTotal = new BigDecimal(total).multiply(Constants.USER_RATE).longValue(); + //抽成 + Long platformFee = total - incomeTotal; + GiftRewardRecord giftRewardRecord = GiftRewardRecord.builder() + .fromUid(fromUid) + .toUid(toUid) + .orderNo(orderNo) + .giftId(giftDict.getId()) + .giftName(giftDict.getName()) + .price(giftDict.getPrice()) + .num(num) + .total(giftDict.getPrice() * num) + .incomeTotal(incomeTotal) + .platformFee(platformFee) + .build(); + save(giftRewardRecord); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HeartbeatLevelDictServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HeartbeatLevelDictServiceImpl.java new file mode 100644 index 0000000..874e30b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HeartbeatLevelDictServiceImpl.java @@ -0,0 +1,97 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.sonic.frog.dao.HeartbeatLevelDictDao; +import com.sonic.frog.domain.entity.HeartbeatLevelDict; +import com.sonic.frog.domain.output.HeartbeatLevelDictOutput; +import com.sonic.frog.enums.HeartbeatLevelEnum; +import com.sonic.frog.service.HeartbeatLevelDictService; +import com.sonic.frog.utils.BeanConver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 心动等级字典表服务实现类 + */ +@Service +@Slf4j +public class HeartbeatLevelDictServiceImpl extends ServiceImpl implements HeartbeatLevelDictService { + public List getUnlockListByCode(HeartbeatLevelEnum code) { + if (code == null) { + return Collections.emptyList(); + } + HeartbeatLevelDict heartbeatLevelDict = getOne(Wrappers.lambdaQuery().eq(HeartbeatLevelDict::getCode, code).last("limit 1")); + if (heartbeatLevelDict != null) { + String unlockCode = heartbeatLevelDict.getUnlockCode(); + return JSON.parseArray(unlockCode, HeartbeatLevelEnum.class); + } + return Collections.emptyList(); + } + + @Override + public List getHearbeatLevelDictList(HeartbeatLevelEnum aiUserHeartbeatLevel) { + //获取字典列表 + List list = list(Wrappers.lambdaQuery() + .eq(HeartbeatLevelDict::getIsDelete, false) + .orderByAsc(HeartbeatLevelDict::getSort) + ); + //获取用户解锁的心动等级 + List unlockList = getUnlockListByCode(aiUserHeartbeatLevel); + //构建输出 + List heartbeatLevelDictOutputList = Lists.newArrayList(); + for (HeartbeatLevelDict heartbeatLevelDict : list) { + HeartbeatLevelDictOutput heartbeatLevelDictOutput = BeanConver.copeBean(heartbeatLevelDict, HeartbeatLevelDictOutput.class); + //是否解锁 + boolean isUnlock = unlockList.contains(heartbeatLevelDict.getCode()); + heartbeatLevelDictOutput.setIsUnlock(isUnlock); + //未解锁,显示未解锁图片 + if (!isUnlock) { + heartbeatLevelDictOutput.setImgUrl(heartbeatLevelDict.getUnlockImgUrl()); + } + heartbeatLevelDictOutput.setNum(heartbeatLevelDictOutput.getCode().getNum()); + heartbeatLevelDictOutputList.add(heartbeatLevelDictOutput); + } + return heartbeatLevelDictOutputList; + } + + @Override + public HeartbeatLevelDict getHeartbeatLevelByVal(BigDecimal heartbeatVal) { + return getOne(Wrappers.lambdaQuery() + .le(HeartbeatLevelDict::getStartVal, heartbeatVal) + .ge(HeartbeatLevelDict::getEndVal, heartbeatVal) + .last("limit 1") + ); + } + + @Override + public HeartbeatLevelDict getHeartbeatLevelDictByLevel(HeartbeatLevelEnum heartbeatLevelEnum) { + return getOne(Wrappers.lambdaQuery() + .eq(HeartbeatLevelDict::getCode, heartbeatLevelEnum) + .last("limit 1") + ); + } + + @Override + public Map mapByLevel(Set heartbeatLevelEnums) { + if (CollectionUtils.isEmpty(heartbeatLevelEnums)) { + return Collections.emptyMap(); + } + List list = list(Wrappers.lambdaQuery().in(HeartbeatLevelDict::getCode, heartbeatLevelEnums)); + if (CollectionUtils.isNotEmpty(list)) { + return list.stream().collect(Collectors.toMap(HeartbeatLevelDict::getCode, Function.identity())); + } + return Collections.emptyMap(); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HomeClassificationServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HomeClassificationServiceImpl.java new file mode 100644 index 0000000..18b7efb --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HomeClassificationServiceImpl.java @@ -0,0 +1,361 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Lists; +import com.sonic.frog.dao.AiUserDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.input.AgeRange; +import com.sonic.frog.domain.input.ClassificationListInput; +import com.sonic.frog.domain.output.AiUserBaseOutput; +import com.sonic.frog.domain.output.ClassificationListOutput; +import com.sonic.frog.enums.AgeTypeEnum; +import com.sonic.frog.service.AiUserSearchService; +import com.sonic.frog.service.AiUserStatService; +import com.sonic.frog.service.HomeClassificationService; +import com.sonic.frog.utils.BeanConver; +import com.sonic.frog.utils.CacheUtils; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class HomeClassificationServiceImpl implements HomeClassificationService { + + @Autowired + private AiUserDao aiUserDao; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private CacheUtils cacheUtils; + + /** + * 每页最多拉取20条数据 + */ + private final int MAX_PS = 20; + + /** + * 20条数据中每个档位最多取4个 + */ + private final int PER_LEVEL_MAX = 4; + + /** + * 每个档位最多取100个 + */ + private final int PER_LEVEL_LIMIT = 100; + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private AiUserStatService aiUserStatService; + + /** + * 每一次推荐取20张卡片 + * 按照被喜欢数进行阶梯取卡片 + * 0个-取4个 + * 1-20-取4个 + * 21-50-取4个 + * 51-100-取4个 + * 大于100-取4个 + * 如果高档位没有,就像低档位取,依次取到最低档位 + * 每一次推荐将取出来的角色在20条内随机排列 + * 公开列表不包含被删除的,私密的,可以包含登录的创建者的 + * + * @param userId + * @param input + * @return + */ + @Override + public List classificationList(Long userId, ClassificationListInput input) { + //超过25页,直接返回空 + if (input.getPn() > 25) { + return Lists.newArrayList(); + } + //限制ps最大为20 + input.setPs(input.getPs() > MAX_PS ? MAX_PS : input.getPs()); + //1. 按档位查询aiId列表, 每个档位预先查询100个,相当于最多查询25页 + // 用户刚进来的,默认查询类别(角色)所有,选择情感分类,标签为情感所有标签,定时任务缓存,定时任务每1分钟更新一次缓存,缓存时间1小时, 每次用户进来都从缓存获取 + // 用户选择条件,缓存到redis,缓存时间1小时,后面查询从缓存中查询 + List likedListGt100 = queryAiIdsByLikedRange(input, 101, Integer.MAX_VALUE, PER_LEVEL_LIMIT, false); + List likedList51to100 = queryAiIdsByLikedRange(input, 51, 100, PER_LEVEL_LIMIT, false); + List likedList21To50 = queryAiIdsByLikedRange(input, 21, 50, PER_LEVEL_LIMIT, false); + List likedList1To20 = queryAiIdsByLikedRange(input, 1, 20, PER_LEVEL_LIMIT, false); + List likedList0 = queryAiIdsByLikedRange(input, 0, 0, 500, false); + log.info("===> classificationList pn:{},likedListGt100:{}", input.getPn(), JSON.toJSONString(likedListGt100)); + log.info("===> classificationList pn:{},likedList51to100:{}", input.getPn(), JSON.toJSONString(likedList51to100)); + log.info("===> classificationList pn:{},likedList21To50:{}", input.getPn(), JSON.toJSONString(likedList21To50)); + log.info("===> classificationList pn:{},likedList1To20:{}", input.getPn(), JSON.toJSONString(likedList1To20)); + log.info("===> classificationList pn:{},likedList0:{}", input.getPn(), JSON.toJSONString(likedList0)); + + // 2. 过滤已查看过的AI(排除exList中的ID) + List filteredGt100 = filterExcluded(likedListGt100, input.getExList()); + List filtered51to100 = filterExcluded(likedList51to100, input.getExList()); + List filtered21To50 = filterExcluded(likedList21To50, input.getExList()); + List filtered1To20 = filterExcluded(likedList1To20, input.getExList()); + List filtered0 = filterExcluded(likedList0, input.getExList()); + + log.info("===> classificationList pn:{},filteredGt100:{}", input.getPn(), JSON.toJSONString(filteredGt100)); + log.info("===> classificationList pn:{},filtered51to100:{}", input.getPn(), JSON.toJSONString(filtered51to100)); + log.info("===> classificationList pn:{},filtered21To50:{}", input.getPn(), JSON.toJSONString(filtered21To50)); + log.info("===> classificationList pn:{},filtered1To20:{}", input.getPn(), JSON.toJSONString(filtered1To20)); + log.info("===> classificationList pn:{},filtered0:{}", input.getPn(), JSON.toJSONString(filtered0)); + + // 3. 按优先级取数:高档位优先,每个档位最多取4个,总取20个 + List selectedAiIds = selectAiIdsByPriority(filteredGt100, filtered51to100, filtered21To50, filtered1To20, filtered0, + PER_LEVEL_MAX, // 每个档位最多取4个 + MAX_PS // 总数量 + ); + + log.info("===> classificationList pn:{},selectedAiIds:{}", input.getPn(), JSON.toJSONString(selectedAiIds)); + + // 4. 查询选中AI的详细信息(实际场景中这里是批量查询数据库) + List outputList = buildOutputList(selectedAiIds); + + // 5. 随机排列结果 + Collections.shuffle(outputList); + + return outputList; + } + + /** + * 按被喜欢数范围查询AIID + * 1. 筛选条件:role_code、character_code、tag_code(input中获取) + * 2. 筛选被喜欢数在[minLiked, maxLiked]之间 + * 3. 排除is_delete=1和permission!=1(非公开)的数据 + * 4. 限制查询数量为limit + * + * @param input 包含查询条件(role_code等) + * @param minLiked 最小被喜欢数 + * @param maxLiked 最大被喜欢数 + * @param limit 最多查询数量 + * @return AIID列表 + */ + private List queryAiIdsByLikedRange(ClassificationListInput input, int minLiked, int maxLiked, int limit, boolean isUpdateCache) { + log.info("===> queryAiIdsByLikedRange input:{}", JSON.toJSONString(input)); + //年龄多选处理 + List ageRangeList = Lists.newArrayList(); + if (CollectionUtils.isNotEmpty(input.getAgeList())) { + for (AgeTypeEnum ageTypeEnum : input.getAgeList()) { + ageRangeList.add(new AgeRange(LocalDateTime.now().minusYears(ageTypeEnum.getEndAge()), LocalDateTime.now().minusYears(ageTypeEnum.getStartAge()))); + } + } + input.setAgeRangeList(ageRangeList); + //缓存键 + String codeStr = JSON.toJSONString(Lists.newArrayList(input.getRoleCodeList(), input.getCharacterCodeList(), input.getTagCodeList())); + String redisKey = redisKeyUtils.homeClassificationCacheKey(codeStr, input.getSexList(), input.getAgeList(), minLiked, maxLiked); + log.info("===> queryAiIdsByLikedRange redisKey:{}", redisKey); + if (!isUpdateCache) { + //缓存1小时 + return cacheUtils.getCacheListAndSet(redisKey, Long.class, () -> { + List aiIdList = aiUserDao.queryAiIdsByLikedRange(input, minLiked, maxLiked, limit); + return JSON.toJSONString(aiIdList); + }, 60 * 60); + } else { + //强制更新缓存 + List aiIdList = aiUserDao.queryAiIdsByLikedRange(input, minLiked, maxLiked, limit); + cacheUtils.setCache(redisKey, JSON.toJSONString(aiIdList), 60 * 60); + return aiIdList; + } + } + + /** + * 过滤已查看过的AI + * + * @param aiIds 原始AIID列表 + * @param excludedIds 已查看的AIID列表 + * @return 过滤后的AIID列表 + */ + private List filterExcluded(List aiIds, List excludedIds) { + if (aiIds == null || aiIds.isEmpty()) { + return Lists.newArrayList(); + } + if (excludedIds == null || excludedIds.isEmpty()) { + return new ArrayList<>(aiIds); + } + // 利用HashSet优化contains判断(O(1)复杂度) + Set excludedSet = new HashSet<>(excludedIds); + return aiIds.stream().filter(aiId -> !excludedSet.contains(aiId)).collect(Collectors.toList()); + } + + /** + * 按优先级(高档位优先)选取AIID + * + * @param gt100 档位5(>100) + * @param fiftyOneTo100 档位4(51-100) + * @param twentyOneTo50 档位3(21-50) + * @param oneTo20 档位2(1-20) + * @param zero 档位1(0) + * @param perLevelMax 每个档位最多选取数量 + * @param totalMax 总选取数量 + * @return 选中的AIID列表 + */ + private List selectAiIdsByPriority(List gt100, List fiftyOneTo100, List twentyOneTo50, List oneTo20, List zero, int perLevelMax, int totalMax) { + List result = Lists.newArrayListWithCapacity(totalMax); + List levelSubList1 = removePreLevelSubListAndGetLevelSubList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, perLevelMax, Lists.newArrayList(), 0); + List levelSubList2 = removePreLevelSubListAndGetLevelSubList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, perLevelMax, levelSubList1, 1); + List levelSubList3 = removePreLevelSubListAndGetLevelSubList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, perLevelMax, levelSubList2, 2); + List levelSubList4 = removePreLevelSubListAndGetLevelSubList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, perLevelMax, levelSubList3, 3); + List levelSubList5 = removePreLevelSubListAndGetLevelSubList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, perLevelMax, levelSubList4, 4); + log.info("levelSubList1:{}", JSON.toJSONString(levelSubList1)); + log.info("levelSubList2:{}", JSON.toJSONString(levelSubList2)); + log.info("levelSubList3:{}", JSON.toJSONString(levelSubList3)); + log.info("levelSubList4:{}", JSON.toJSONString(levelSubList4)); + log.info("levelSubList5:{}", JSON.toJSONString(levelSubList5)); + result.addAll(levelSubList1); + result.addAll(levelSubList2); + result.addAll(levelSubList3); + result.addAll(levelSubList4); + result.addAll(levelSubList5); + return result; + } + + /** + * 根据aiId列表构建输出对象列表 + * + * @param aiIds AIID列表 + * @return 输出对象列表 + */ + private List buildOutputList(List aiIds) { + if (aiIds.isEmpty()) { + return Lists.newArrayList(); + } + //批量获取ai基础信息Map + Map aiUserMap = aiUserSearchService.mapByAiIdList(aiIds); + //批量获取被喜欢数Map + Map aiLikedCountMap = aiUserStatService.queryAiLikedCount(aiIds); + return aiIds.stream().map(aiId -> { + AiUserBaseOutput aiUserBaseOutput = aiUserMap.get(aiId); + //基础信息赋值 + ClassificationListOutput output = BeanConver.copeBean(aiUserBaseOutput, ClassificationListOutput.class); + if (output != null) { + //被喜欢数赋值 + output.setLikedNum(aiLikedCountMap.get(aiId) != null ? aiLikedCountMap.get(aiId) : 0); + } + return output; + }).collect(Collectors.toList()); + } + + @Override + public void homeClassificationUpdateCache(ClassificationListInput input) { + queryAiIdsByLikedRange(input, 101, Integer.MAX_VALUE, PER_LEVEL_LIMIT, true); + queryAiIdsByLikedRange(input, 51, 100, PER_LEVEL_LIMIT, true); + queryAiIdsByLikedRange(input, 21, 50, PER_LEVEL_LIMIT, true); + queryAiIdsByLikedRange(input, 1, 20, PER_LEVEL_LIMIT, true); + queryAiIdsByLikedRange(input, 0, 0, PER_LEVEL_LIMIT, true); + } + + public static void main(String[] args) { + List gt100 = Lists.newArrayList(441983024168961L, 439266683584513L, 439266631155713L); + List fiftyOneTo100 = Lists.newArrayList(); + List twentyOneTo50 = Lists.newArrayList(436963048357889L, 436963289530369L); + List oneTo20 = Lists.newArrayList(436963121758209L, 436963067232257L, 436963222421505L, 437080281317377L, 441273826344961L, 437109194752001L, 437112596332545L, 437304385077249L, 437316674387969L, 443040313704449L, 437316762468353L, 437242762362881L, 437261680771073L, 437285514903553L, 440185938509825L, 437248804257793L, 440192452263937L, 437069334249473L, 442709930672129L, 440190650810369L, 437305477693441L, 437458928140289L, 442591569510401L, 440187821752321L, 437464559255553L, 438934010658817L, 440193175781377L, 437416915828737L, 441103465906177L, 436963721543681L, 437470041210881L, 441270219243521L, 441992207138817L, 442723671277569L, 437316691165185L, 444208335486977L, 440187649785857L, 440190927634433L, 445795826794497L, 445797259149313L, 437309766369281L, 440197082775553L, 446189583466497L, 446190357315585L, 439217670979585L, 445443379560449L, 445802852319233L, 446192550936577L, 445388404752385L, 445438184914945L, 446196738949121L, 446175786303489L); + List zero = Lists.newArrayList(); + selectAiIdsByPriorityTest(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, 4, 20); + } + + private static List selectAiIdsByPriorityTest(List gt100, List fiftyOneTo100, List twentyOneTo50, List oneTo20, List zero, int perLevelMax, int totalMax) { + List result = Lists.newArrayListWithCapacity(totalMax); + //从高档位开始取,依次从高档位取4个,高档位没有,从低档位取 + List levelSubList1 = removePreLevelSubListAndGetLevelSubList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, perLevelMax, Lists.newArrayList(), 0); + List levelSubList2 = removePreLevelSubListAndGetLevelSubList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, perLevelMax, levelSubList1, 1); + List levelSubList3 = removePreLevelSubListAndGetLevelSubList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, perLevelMax, levelSubList2, 2); + List levelSubList4 = removePreLevelSubListAndGetLevelSubList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, perLevelMax, levelSubList3, 3); + List levelSubList5 = removePreLevelSubListAndGetLevelSubList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero, perLevelMax, levelSubList4, 4); + System.out.println("levelSubList1:" + JSON.toJSONString(levelSubList1)); + System.out.println("levelSubList2:" + JSON.toJSONString(levelSubList2)); + System.out.println("levelSubList3:" + JSON.toJSONString(levelSubList3)); + System.out.println("levelSubList4:" + JSON.toJSONString(levelSubList4)); + System.out.println("levelSubList5:" + JSON.toJSONString(levelSubList5)); + result.addAll(levelSubList1); + result.addAll(levelSubList2); + result.addAll(levelSubList3); + result.addAll(levelSubList4); + result.addAll(levelSubList5); + return result; + } + + + /** + * 移除前上一级子列表,并获取当前级别子列表 + * + * @param gt100 + * @param fiftyOneTo100 + * @param twentyOneTo50 + * @param oneTo20 + * @param zero + * @param perLevelMax + * @param preLevelSubList + * @param start + * @return + */ + private static List removePreLevelSubListAndGetLevelSubList(List gt100, List fiftyOneTo100, List twentyOneTo50, List oneTo20, List zero, int perLevelMax, List preLevelSubList, Integer start) { + //各档位先移除前一级子列表,然后获取当前级别子列表 + gt100.removeAll(preLevelSubList); + fiftyOneTo100.removeAll(preLevelSubList); + twentyOneTo50.removeAll(preLevelSubList); + oneTo20.removeAll(preLevelSubList); + zero.removeAll(preLevelSubList); + //移除后重新拼装的各档位列表 + List> levelList = Lists.newArrayList(gt100, fiftyOneTo100, twentyOneTo50, oneTo20, zero); + //获取当前级别子列表,如果当前没有,递归向低档位获取 + List levelSubList = getLevelSubList(levelList, perLevelMax, start); + //当前档位或低档位中没有获取到或不足4个,从总的列表剩余中获取不足的个数 + if (CollectionUtils.isEmpty(levelSubList) || (CollectionUtils.isNotEmpty(levelSubList) && levelSubList.size() < perLevelMax)) { + //总的剩余列表 + List totalList = Lists.newArrayList(); + totalList.addAll(gt100); + totalList.addAll(fiftyOneTo100); + totalList.addAll(twentyOneTo50); + totalList.addAll(oneTo20); + totalList.addAll(zero); + //移除已获取的 + if (CollectionUtils.isNotEmpty(levelSubList)) { + totalList.removeAll(levelSubList); + } + //满足remainNum个,直接取,不足从总的列表中取 + int remainNum = perLevelMax - levelSubList.size(); + if (totalList.size() >= remainNum) { + levelSubList.addAll(totalList.subList(0, remainNum)); + } else { + levelSubList.addAll(totalList.subList(0, totalList.size())); + } + } + return levelSubList; + } + + /** + * 获取当前级别子列表 + * + * @param levelList + * @param perLevelMax + * @param start + * @return + */ + private static List getLevelSubList(List> levelList, int perLevelMax, Integer start) { + List levelSubList = Lists.newArrayList(); + for (int i = start; i < levelList.size(); i++) { + List currentLevelList = levelList.get(i); + Integer remainNum = perLevelMax - currentLevelList.size(); + if (currentLevelList.size() < perLevelMax) { + //添加本档位的数量 + levelSubList.addAll(currentLevelList); + //不够,从下一个档位补全 + List nextLevelSubList = getLevelSubList(levelList, remainNum, i + 1); + levelSubList.addAll(nextLevelSubList); + break; + } else { + //够,直接返回 + levelSubList.addAll(currentLevelList.subList(0, perLevelMax)); + break; + } + } + return levelSubList; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HomeRecommendServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HomeRecommendServiceImpl.java new file mode 100644 index 0000000..41dc320 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HomeRecommendServiceImpl.java @@ -0,0 +1,248 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.frog.dao.AiUserAlbumDao; +import com.sonic.frog.dao.AiUserDao; +import com.sonic.frog.domain.bo.AIUserAlbumGroupBo; +import com.sonic.frog.domain.bo.RandomMeetRateBo; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.UserAiMeet; +import com.sonic.frog.domain.input.AgeRange; +import com.sonic.frog.domain.input.HomeRecommendInput; +import com.sonic.frog.domain.output.HomeRecommendOutput; +import com.sonic.frog.domain.output.ListAiAlbumOutput; +import com.sonic.frog.enums.AgeTypeEnum; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.LimitUtils; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Period; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class HomeRecommendServiceImpl implements HomeRecommendService { + + @Autowired + private UserAiMeetService userAiMeetService; + @Autowired + private AiUserService aiUserService; + @Autowired + private AiDictService aiDictService; + @Autowired + private AiUserStatService aiUserStatService; + @Autowired + private AiUserDao aiUserDao; + @Autowired + private UserService userService; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private LimitUtils limitUtils; + @Autowired + private AiUserAlbumDao aiUserAlbumDao; + @Autowired + private AiUserAlbumService userAlbumService; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + + @Override + public List recommendList(Long currentUserId, HomeRecommendInput input) { + //初始化meet概率数据,并存储到redis中 + initMeetRate(currentUserId); + //查询出已经匹配上的AI,需要进行排除掉 + if (currentUserId != null) { + List exAiIdList = userAiMeetService.list(Wrappers.lambdaQuery().select(UserAiMeet::getAiId).eq(UserAiMeet::getUserId, currentUserId)) + .stream().map(UserAiMeet::getAiId).collect(Collectors.toList()); + //将前端透传的需要排除的AI列表添加到需要排除的AI列表中 + input.getExList().addAll(exAiIdList); + //当前端用户没有输入性别,则尝试获取当前用户性别 + if (CollectionUtils.isEmpty(input.getSexList())) { + //获取当前用户的性别,女推男,男推女 + BaseUserInfoOutput baseUserInfoOutput = userService.baseUserInfo(currentUserId); + if (baseUserInfoOutput != null && baseUserInfoOutput.getSex() != null) { + Integer sex = null; + if (baseUserInfoOutput.getSex() != null && baseUserInfoOutput.getSex() == 0) { + sex = 1; + } else if (baseUserInfoOutput.getSex() != null && baseUserInfoOutput.getSex() == 1) { + sex = 0; + } + //设置性别列表 多选 + if (sex == null) { + input.setSexList(Lists.newArrayList(0,1,2)); + } else { + input.setSexList(Lists.newArrayList(sex)); + } + } + } + + } + //年龄多选处理 + List ageRangeList = Lists.newArrayList(); + if (CollectionUtils.isNotEmpty(input.getAgeList())) { + for (AgeTypeEnum ageTypeEnum : input.getAgeList()) { + ageRangeList.add(new AgeRange(LocalDateTime.now().minusYears(ageTypeEnum.getEndAge()), LocalDateTime.now().minusYears(ageTypeEnum.getStartAge()))); + } + } + input.setAgeRangeList(ageRangeList); + + log.info("recommendList input:{}", JSON.toJSONString(input)); + //随机查询一批AI列表出来给前端,随机数 + List randomAiIdList = aiUserDao.randomGetAiUser(currentUserId, input.getExList(), input.getRoleCodeList(), input.getRandom(), input.getPs(), input.getSexList(), input.getAgeRangeList()); + return getHomeRecommendOutputs(currentUserId, randomAiIdList); + } + + @NotNull + private List getHomeRecommendOutputs(Long currentUserId, List randomAiIdList) { + if (CollectionUtils.isEmpty(randomAiIdList)) { + return Lists.newArrayList(); + } + //批量查询AI基础信息出来 + List aiUserList = aiUserService.list(Wrappers.lambdaQuery().in(AiUser::getAiId, randomAiIdList)); + //构造批量字典列表 + Set dictCodeList = Sets.newHashSet(); + aiUserList.forEach(aiUser -> { + dictCodeList.add(aiUser.getRoleCode()); + dictCodeList.add(aiUser.getCharacterCode()); + dictCodeList.add(aiUser.getTagCode()); + }); + //批量获取字典信息 + Map aiDictNameMap = aiDictService.mapNameByCodeList(dictCodeList); + Map aiLikedCountMap = aiUserStatService.queryAiLikedCount(randomAiIdList); + //批量获取AI的相册列表 + Map> albumListMap = buildAlbumListMap(currentUserId, randomAiIdList); + //批量获取用户和AI的温度值 + Map aiHeartbeatValMap = aiUserHeartbeatRelationService.queryAIHeartbeatVal(currentUserId, randomAiIdList); + + List outputList = Lists.newArrayList(); + aiUserList.forEach(aiUser -> { + HomeRecommendOutput output = new HomeRecommendOutput(); + output.setAiId(aiUser.getAiId()); + output.setNickname(aiUser.getNickname()); + output.setSex(aiUser.getSex()); + output.setHeadImg(aiUser.getHeadImg()); + //计算年龄 + output.setAge(aiUser.getBirthday() == null ? 0 : Period.between(aiUser.getBirthday().toLocalDate(), LocalDate.now()).getYears()); + output.setRole(aiDictNameMap.get(aiUser.getRoleCode())); + output.setCharacter(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTag(aiDictNameMap.get(aiUser.getTagCode())); + output.setIntroduction(aiUser.getIntroduction()); + output.setImageUrl(aiUser.getHomeImageUrl()); + //设置点赞数 + output.setLikedCount(aiLikedCountMap.get(aiUser.getAiId()) == null ? 0 : aiLikedCountMap.get(aiUser.getAiId())); + output.setAlbumList(albumListMap.get(aiUser.getAiId())); + output.setHeartbeatVal(aiHeartbeatValMap.get(aiUser.getAiId())); + outputList.add(output); + }); + //将outputList按照randomAiIdList的顺序进行排列 + outputList.sort(Comparator.comparingInt(o -> randomAiIdList.indexOf(o.getAiId()))); + return outputList; + } + + /** + * 批量获取AI的相册列表 + * + * @param currentUserId + * @param aiIdList + * @return + */ + private Map> buildAlbumListMap(Long currentUserId, List aiIdList) { + if (CollectionUtils.isEmpty(aiIdList)) { + return new HashMap<>(0); + } + //批量获取AI的id,然后每个只取出最开始的8个 + List listAlbumsGroup = aiUserAlbumDao.listAlbumsGroup(aiIdList); + Map> idListMap = new HashMap<>(20); + List allIdList = Lists.newArrayList(); + //批量获取基础数据 + for (AIUserAlbumGroupBo bo : listAlbumsGroup) { + List idList = Lists.newArrayList(bo.getIdListStr().split(",")); + //根据id降序排列 + idList.sort(Comparator.reverseOrder()); + //只获取前8条数据 + idList = idList.subList(0, Math.min(idList.size(), 8)); + List longIdList = Lists.newArrayList(); + idList.forEach(e -> longIdList.add(Long.valueOf(e))); + allIdList.addAll(longIdList); + idListMap.put(bo.getAiId(), longIdList); + } + //批量获取图片数据 + List listByIds = userAlbumService.listByIds(currentUserId, allIdList); + //先根据id进行降序排列,然后根据aiId进行分组处理 + listByIds.sort(Comparator.comparing(ListAiAlbumOutput::getAlbumId).reversed()); + //根据用户分组,然后根据id进行降序排列 + Map> listMap = listByIds.stream().collect(Collectors.groupingBy(ListAiAlbumOutput::getAiId)); + return listMap; + } + + + @Override + public HomeRecommendOutput getAiMeetDetail(Long currentUserId, Long aiId) { + List list = getHomeRecommendOutputs(currentUserId, Lists.newArrayList(aiId)); + return CollectionUtils.isEmpty(list) ? null : list.get(0); + } + + /** + * 初始化meet概率数据 + * + * @param currentUserId + */ + private void initMeetRate(Long currentUserId) { + if (currentUserId == null) { + return; + } + //判断请求次数 + int requestCount = limitUtils.defaultLimitCheckReturnCount(redisKeyUtils.meetSdCount(currentUserId), 2 * 60 * 60); + log.info("===> initMeetRate requestCount : {}", requestCount); + //第一次请求的话,初始化概率数据,并存储到redis中 + if (requestCount == 1) { + //生成概率数据,并存到redis中 + RandomMeetRateBo randomMeetRateBo = RandomMeetRateBo.builder() + .bl1120(generateRandom(1, 100)) + .bl3140(generateRandom(1, 30)) + .bl4150(generateRandom(1, 20)) + .bl5160(generateRandom(1, 10)) + .bl6170(generateRandom(1, 5)) + .build(); + String redisValue = JSONObject.toJSONString(randomMeetRateBo).trim(); + log.info("===> initMeetRate redisValue : {}", redisValue); + stringRedisTemplate.opsForValue().set(redisKeyUtils.meetRateBoKey(currentUserId), redisValue, 2 * 60 * 60, TimeUnit.SECONDS); + } + } + + /** + * 生成随机数是否命中 + * + * @param startNumber + * @param endNumber + * @return + */ + public boolean generateRandom(int startNumber, int endNumber) { + Random random = new Random(); + // 生成1-100之间的随机数 + int randomNumber = random.nextInt(100) + 1; + if (randomNumber >= startNumber && randomNumber <= endNumber) { + return true; + } + return false; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HomeRecommendV2ServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HomeRecommendV2ServiceImpl.java new file mode 100644 index 0000000..ce9d3e7 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/HomeRecommendV2ServiceImpl.java @@ -0,0 +1,147 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Lists; +import com.sonic.frog.dao.AiUserDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.AiUserExt; +import com.sonic.frog.domain.entity.AiUserStat; +import com.sonic.frog.domain.output.*; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class HomeRecommendV2ServiceImpl implements HomeRecommendV2Service { + + @Autowired + private RankService rankService; + @Autowired + private AiUserService aiUserService; + @Autowired + private AiUserExtService aiUserExtService; + @Autowired + private AiUserStatService aiUserStatService; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private AiUserDao aiUserDao; + + @Override + public void updateCache() { + //处理 mostChat 的缓存数据 + List mostChatList = rankService.chatRank(null, 20); + //处理 mustCrush 的缓存数据 + List mostCrushList = rankService.aiHeartbeatRank(null, 20); + //处理 starAChat 的缓存数据 + List startChatList = startChatList(); + //处理 mustGifted 的缓存数据 + List mostGiftedList = rankService.giftRank(null, 20); + //将数据缓存到redis中 + HomeRecommendV2Output output = new HomeRecommendV2Output(); + output.setMostChat(mostChatList); + output.setMustCrush(mostCrushList); + output.setStarAChat(startChatList); + output.setMustGifted(mostGiftedList); + //写入redis缓存1天 + stringRedisTemplate.opsForValue().set(redisKeyUtils.homeRecommendV2CacheKey(), JSONObject.toJSONString(output), 1, TimeUnit.DAYS); + } + + private List startChatList() { + log.info("===> startChatList start"); + List outputList = Lists.newArrayList(); + List idList1 = aiUserDao.homeRecommend(true, 20); + //数量不够,补数 + if(CollectionUtils.isEmpty(idList1) || idList1.size() < 4) { + List idList2 = aiUserDao.homeRecommend(false, 20 - idList1.size()); + idList1.addAll(idList2); + } + if(CollectionUtils.isEmpty(idList1)) { + log.info("mostChatList is empty 1"); + return Collections.emptyList(); + } + List aiUserList1 = aiUserService.list(Wrappers.lambdaQuery().in(AiUser::getAiId, idList1)); + if(CollectionUtils.isEmpty(aiUserList1)) { + log.info("mostChatList is empty 2"); + return Collections.emptyList(); + } + List aiIdList = aiUserList1.stream().map(AiUser::getAiId).collect(Collectors.toList()); + //批量查询扩展信息 + List aiUserExtList = aiUserExtService.list(Wrappers.lambdaQuery() + .select(AiUserExt::getAiId, AiUserExt::getDialoguePrologueSound, AiUserExt::getSupportingContent) + .in(AiUserExt::getAiId, aiIdList)); + Map aiUserExtMap = aiUserExtList.stream().collect(Collectors.toMap(AiUserExt::getAiId, aiUserExt -> aiUserExt)); + //批量查询点赞数 + List aiUserStatList = aiUserStatService.list(Wrappers.lambdaQuery().in(AiUserStat::getAiId, aiIdList)); + Map aiUserStatMap = aiUserStatList.stream().collect(Collectors.toMap(AiUserStat::getAiId, aiUserStat -> aiUserStat)); + for (AiUser aiUser : aiUserList1) { + AiUserExt aiUserExt = aiUserExtMap.get(aiUser.getAiId()); + AiUserStat aiUserStat = aiUserStatMap.get(aiUser.getAiId()); + StartChatOutput output = new StartChatOutput(); + output.setAiId(aiUser.getAiId()); + output.setNickname(aiUser.getNickname()); + output.setHeadImg(aiUser.getHeadImg()); + if(aiUserExt != null) { + output.setDialoguePrologueSound(aiUserExt.getDialoguePrologueSound()); + output.setSupportingContentList(StringUtils.isEmpty(aiUserExt.getSupportingContent()) ? null : JSONObject.parseArray(aiUserExt.getSupportingContent(), String.class)); + } + output.setLikedNum(aiUserStat == null ? 0 : aiUserStat.getLikedNum()); + output.setCreateTime(aiUser.getCreateTime()); + outputList.add(output); + } + return outputList; + } + + @Override + public HomeRecommendV2Output recommendList() { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKeyUtils.homeRecommendV2CacheKey()); + if(StringUtils.isEmpty(cacheStr)) { + return null; + } + HomeRecommendV2Output cacheObject = JSONObject.parseObject(cacheStr, HomeRecommendV2Output.class); + //处理 + HomeRecommendV2Output output = new HomeRecommendV2Output(); + //随机取20个 + if(CollectionUtils.isNotEmpty(cacheObject.getMostChat())) { + Collections.shuffle(cacheObject.getMostChat()); + output.setMostChat(cacheObject.getMostChat()); + } + if(CollectionUtils.isNotEmpty(cacheObject.getMustCrush())) { + Collections.shuffle(cacheObject.getMustCrush()); + output.setMustCrush(cacheObject.getMustCrush()); + } + if(CollectionUtils.isNotEmpty(cacheObject.getStarAChat())) { + //将数据进行分组处理,3天内创建的在前面,三天后创建的在后面 + List list1 = cacheObject.getStarAChat().stream().filter(item -> item.getCreateTime().isAfter(LocalDateTime.now().minusDays(3))).collect(Collectors.toList()); + List list2 = cacheObject.getStarAChat().stream().filter(item -> item.getCreateTime().isBefore(LocalDateTime.now().minusDays(3))).collect(Collectors.toList()); + Collections.shuffle(list1); + Collections.shuffle(list2); + list1.addAll(list2); + List list = list1.size() > 4 ? list1.subList(0, 4) : list1; + Collections.shuffle(list); + output.setStarAChat(list); + } + if(CollectionUtils.isNotEmpty(cacheObject.getMustGifted())) { + Collections.shuffle(cacheObject.getMustGifted()); + output.setMustGifted(cacheObject.getMustGifted()); + } + return output; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ImageStyleDictServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ImageStyleDictServiceImpl.java new file mode 100644 index 0000000..4c99564 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ImageStyleDictServiceImpl.java @@ -0,0 +1,34 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.ImageStyleDictDao; +import com.sonic.frog.domain.entity.ImageStyleDict; +import com.sonic.frog.service.ImageStyleDictService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + *

+ * 形象风格图片表 服务实现类 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +@Service +@Slf4j +public class ImageStyleDictServiceImpl extends ServiceImpl implements ImageStyleDictService { + + @Override + public List getAllImageStyleDictList() { + return list(Wrappers.lambdaQuery().eq(ImageStyleDict::getIsDelete, false).orderByAsc(ImageStyleDict::getSort)); + } + + @Override + public ImageStyleDict getImageStyleDictByCode(String imageStyleCode) { + return getOne(Wrappers.lambdaQuery().eq(ImageStyleDict::getCode, imageStyleCode).last("limit 1")); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/LikedServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/LikedServiceImpl.java new file mode 100644 index 0000000..c975874 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/LikedServiceImpl.java @@ -0,0 +1,68 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.LikedDao; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.Liked; +import com.sonic.frog.domain.input.AiUserLikeOrCancelInput; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.event.inner.payload.AiUserStatPayload; +import com.sonic.frog.service.AiUserSearchService; +import com.sonic.frog.service.CommonSendMqService; +import com.sonic.frog.service.LikedService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 点赞业务实现类 + */ +@Service +@Slf4j +public class LikedServiceImpl extends ServiceImpl implements LikedService { + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private LikedDao likedDao; + @Autowired + private CommonSendMqService commonSendMqService; + + @Override + public void aiUserLikeOrCancel(Long userId, AiUserLikeOrCancelInput input) { + //获取ai用户信息 + AiUser aiUser = aiUserSearchService.getAiUserByAiId(input.getAiId()); + ToastResultCode.AI_USER_NOT_EXIST.check(aiUser == null); + //查询点赞信息 + Liked liked = likedDao.selectOne(Wrappers.lambdaQuery() + .eq(Liked::getBizId, input.getAiId()) + .eq(Liked::getBizType, Liked.BizType.AI) + .eq(Liked::getLikedUserId, userId)); + //如果数据库状态与传过来的状态一致,不处理 + if (liked != null && liked.getLikedStatus() == input.getLikedStatus()) { + return; + } + //点赞或取消点赞 + likedDao.likeOrCancel(Liked.builder() + .bizId(input.getAiId()) + .bizType(Liked.BizType.AI) + .aiId(input.getAiId()) + .likedUserId(userId) + .likedStatus(input.getLikedStatus()).build()); + //如果是喜欢或者不喜欢的AI的话,需要发送MQ消息,异步计算喜欢总数 + Liked.LikedStatus likedStatus = input.getLikedStatus(); + boolean lk = likedStatus == Liked.LikedStatus.LIKED; + commonSendMqService.aiUserStatMq(input.getAiId(), lk ? AiUserStatPayload.Type.LIKED : AiUserStatPayload.Type.CANCEL_LIKED, null); + } + + @Override + public Boolean isLiked(Long userId, Long bizId, Liked.BizType bizType) { + int count = count(Wrappers.lambdaQuery() + .eq(Liked::getLikedUserId, userId) + .eq(Liked::getBizId, bizId) + .eq(Liked::getBizType, bizType) + .eq(Liked::getLikedStatus, Liked.LikedStatus.LIKED) + ); + return count > 0; + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/MeetServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/MeetServiceImpl.java new file mode 100644 index 0000000..cad3659 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/MeetServiceImpl.java @@ -0,0 +1,85 @@ +package com.sonic.frog.service.impl; + +import java.time.LocalDateTime; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.alibaba.fastjson.JSON; +import com.sonic.frog.domain.entity.AiUserHeartbeatRelation; +import com.sonic.frog.domain.entity.MeetUnlock; +import com.sonic.frog.domain.input.MeetUnlockInput; +import com.sonic.frog.service.AiUserHeartbeatRelationService; +import com.sonic.frog.service.MeetService; +import com.sonic.frog.service.MeetUnlockService; +import com.sonic.frog.utils.KeyGenerator; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.enums.BizType; +import com.sonic.lion.lib.input.BalanceCheckoutInput; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class MeetServiceImpl implements MeetService { + + @Autowired + private PayClient payClient; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private MeetUnlockService meetUnlockService; + @Transactional(rollbackFor = Exception.class) + @Override + public void meetUnLock(Long currentUserId, MeetUnlockInput input) { + //平台固定费用50 + Long totalAmount = 5000L; + //生成订单编号 + String orderNo = KeyGenerator.instance().generatorUniqueKey(""); + //调用支付服务扣款 + BalanceCheckoutInput balanceCheckoutInput = BalanceCheckoutInput.builder() + .platform("Balance") + .outTradeNo(orderNo) + .bizType(BizType.UNLOCK_ADMIRERS) + .name(BizType.UNLOCK_ADMIRERS.getDesc()) + //付款人 + .srcAccountId(currentUserId) + //收款人 + .desAccountId(-2L) + //收款总金额 + .productAmount(totalAmount) + //平台抽取的金额 + .platformFee(0L) + //折扣金额 + .promoAmount(0L) + .extend(JSON.toJSONString(input)) + .build(); + payClient.checkoutToUser(balanceCheckoutInput); + + //保存关系数据 + AiUserHeartbeatRelation aiUserHeartbeatRelation = aiUserHeartbeatRelationService.getHeartbeatRelation(currentUserId, input.getAiId()); + if(aiUserHeartbeatRelation == null) { + //保存记录 + aiUserHeartbeatRelation = new AiUserHeartbeatRelation(); + aiUserHeartbeatRelation.setUserId(currentUserId); + aiUserHeartbeatRelation.setAiId(input.getAiId()); + aiUserHeartbeatRelation.setCreateTime(LocalDateTime.now()); + aiUserHeartbeatRelation.setEditTime(LocalDateTime.now()); + //首次聊天时间 + aiUserHeartbeatRelation.setFirstChatTime(LocalDateTime.now()); + aiUserHeartbeatRelationService.save(aiUserHeartbeatRelation); + } + MeetUnlock meetUnlock = MeetUnlock.builder() + .userId(currentUserId) + .aiId(input.getAiId()) + .orderNo(orderNo) + .createTime(LocalDateTime.now()) + .build(); + //保存解锁记录 + meetUnlockService.save(meetUnlock); +// //添加meet关系数据 +// userAiMeetService.addMeet(currentUserId, input.getAiId()); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/MeetUnlockServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/MeetUnlockServiceImpl.java new file mode 100644 index 0000000..76028cf --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/MeetUnlockServiceImpl.java @@ -0,0 +1,15 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.MeetUnlockDao; +import com.sonic.frog.domain.entity.MeetUnlock; +import com.sonic.frog.service.MeetUnlockService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class MeetUnlockServiceImpl extends ServiceImpl implements MeetUnlockService { + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/MockServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/MockServiceImpl.java new file mode 100644 index 0000000..54ca69a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/MockServiceImpl.java @@ -0,0 +1,103 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import com.sonic.cow.lib.client.ContentClient; +import com.sonic.cow.lib.enums.PromptTypeEnum; +import com.sonic.cow.lib.input.GenAiUserContentV1Input; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.entity.AiUserExt; +import com.sonic.frog.service.AiUserExtService; +import com.sonic.frog.service.AiUserService; +import com.sonic.frog.service.MockService; +import com.sonic.frog.utils.DateConvertUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class MockServiceImpl implements MockService { + + @Autowired + private AiUserService aiUserService; + @Autowired + private AiUserExtService aiUserExtService; + @Autowired + private ContentClient contentClient; + + @Override + public void updateAiUserExt(Long aiId) throws UnirestException { + //查询出AI基础信息 + AiUser aiUser = aiUserService.getOne(Wrappers.lambdaQuery().eq(AiUser::getAiId, aiId)); + AiUserExt aiUserExt = aiUserExtService.getAiUserExtByAiId(aiId); + + GenAiUserContentV1Input input = GenAiUserContentV1Input.builder() + .nickname(aiUser.getNickname()) + .birthday(DateConvertUtils.format(aiUser.getBirthday())) + .sex(aiUser.getSex().toString()) + .ptType(PromptTypeEnum.GEN_PROFILE_BY_NON) + .characterCode(aiUser.getCharacterCode()) + .tagCode(aiUser.getTagCode()) + .build(); + //一键生成基础信息 + String userProfile = post(input); + + input.setFigure(userProfile); + input.setPtType(PromptTypeEnum.GEN_PROLOGUE_BY_NON); + //一键生成开场白 + String dialoguePrologue = post(input); + + input.setPtType(PromptTypeEnum.GEN_DIALOG_STYLE_BY_NON); + //一键生成对话风格 + String dialogueStyle = post(input); + + input.setPtType(PromptTypeEnum.GEN_INTRODUCTION); + //一键生成人物简介 + String introduction = post(input); + + input.setPtType(PromptTypeEnum.EXTRACT_JSON_CONTENT); + //一键提取json字段 + String userProfileExtJson = post(input); + + //更新数据 + AiUser updateAiUser = AiUser.builder() + .id(aiUser.getId()) + .introduction(introduction) + .build(); + aiUserService.updateById(updateAiUser); + + AiUserExt updateAiUserExt = AiUserExt.builder() + .id(aiUserExt.getId()) + .userProfile(userProfile) + .userProfileExtJson(userProfileExtJson) + .dialogueStyle(dialogueStyle) + .dialoguePrologue(dialoguePrologue) + .build(); + aiUserExtService.updateById(updateAiUserExt); + } + + + /** + * 模拟生成 + * @param input + * @return + * @throws UnirestException + */ + public String post(GenAiUserContentV1Input input) throws UnirestException { + HttpResponse response = Unirest.post("http://test-cow-svc:8080/api/gen/user-content") +// HttpResponse response = Unirest.post("https://test-cow.crushlevel.ai/mock/gen/user-content") + .header("Content-Type", "application/json") + .body(JSONObject.toJSONString(input)) + .asString(); + String body = response.getBody(); + JSONObject jsonObject = JSONObject.parseObject(body); + return jsonObject.getString("content"); + } + + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/PayServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/PayServiceImpl.java new file mode 100644 index 0000000..dcc9d7f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/PayServiceImpl.java @@ -0,0 +1,141 @@ +package com.sonic.frog.service.impl; + +import com.sonic.common.utils.CloudFrontSignerUtils; +import com.sonic.frog.domain.entity.AiUserAlbum; +import com.sonic.frog.domain.entity.AiUserHeartbeatRelation; +import com.sonic.frog.domain.entity.MeetUnlock; +import com.sonic.frog.domain.input.BuyCreateImageCountInput; +import com.sonic.frog.domain.input.UnlockLikeYouInput; +import com.sonic.frog.domain.output.ViewUnlockAlbumImgOutput; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.KeyGenerator; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.enums.BizType; +import com.sonic.lion.lib.input.BalanceCheckoutInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * @description: + * @author: mzc + * @date: 2025-09-10 17:50 + **/ +@Service +@Slf4j +public class PayServiceImpl implements PayService { + + @Autowired + private AiUserAlbumService aiUserAlbumService; + @Autowired + private CloudFrontSignerUtils cloudFrontSignerUtils; + @Autowired + private PayClient payClient; + @Autowired + private BuyCreateCountRecordService buyCreateCountRecordService; + @Autowired + private UserCreateCountStatService userCreateCountStatService; + @Autowired + private AiUserAlbumUnlockService aiUserAlbumUnlockService; + @Autowired + private MeetUnlockService meetUnlockService; + @Autowired + private AiUserHeartbeatRelationService aiUserHeartbeatRelationService; + @Autowired + private UserAiMeetService userAiMeetService; + + @Override + public void buyCreateImageCount(BuyCreateImageCountInput input, Long userId) { + Integer count = input.getCount(); + //调用支付服务扣款 + String orderNo = KeyGenerator.instance().generatorUniqueKey(""); + Long totalAmount = count * 70 * 100L; + BalanceCheckoutInput balanceCheckoutInput = BalanceCheckoutInput.builder() + .platform("Balance") + .outTradeNo(orderNo) + .bizType(BizType.CREATE_AI_IMAGE) + .name(BizType.CREATE_AI_IMAGE.getDesc()) + //付款人 + .srcAccountId(userId) + //收款人 + .desAccountId(-1L) + //总金额 次数*单价*100 + .productAmount(totalAmount) + //折扣金额 + .promoAmount(0L) + .build(); + payClient.checkoutToB(balanceCheckoutInput); + //保存购买记录 + buyCreateCountRecordService.add(userId, totalAmount, count, orderNo); + //给用户增加创作次数-购买次数 + userCreateCountStatService.addBuyNum(userId, count); + } + + @Override + public ViewUnlockAlbumImgOutput unlockLikeYou(UnlockLikeYouInput input, Long userId) { + Long albumId = input.getAlbumId(); + AiUserAlbum aiUserAlbum = aiUserAlbumService.getById(albumId); + ToastResultCode.ALBUM_NOT_EXIST.check(aiUserAlbum == null); + + //调用支付服务扣款 + Long totalAmount = 5000L; + String orderNo = KeyGenerator.instance().generatorUniqueKey(""); + BalanceCheckoutInput balanceCheckoutInput = BalanceCheckoutInput.builder() + .platform("Balance") + .outTradeNo(orderNo) + .bizType(BizType.UNLOCK_ADMIRERS) + .name(BizType.UNLOCK_ADMIRERS.getDesc()) + //付款人 + .srcAccountId(userId) + //收款人 + .desAccountId(-1L) + //总金额 + .productAmount(totalAmount) + //折扣金额 + .promoAmount(0L) + .build(); + payClient.checkoutToB(balanceCheckoutInput); + + //保存相册解锁记录 + aiUserAlbumUnlockService.addUnlockAlbumRecord(userId, albumId, orderNo); + //保存关系数据 + aiUserHeartbeatRelationService.initAiUserHeartbeatRelation(userId, input.getAiId()); + //保存解锁记录 + MeetUnlock meetUnlock = MeetUnlock.builder() + .userId(userId) + .aiId(input.getAiId()) + .orderNo(orderNo) + .createTime(LocalDateTime.now()) + .build(); + meetUnlockService.save(meetUnlock); + //添加meet关系数据 + userAiMeetService.addMeet(userId, input.getAiId()); + + //返回可见的图片 + //解锁,需要ip和过期时间签名的图片 + String fileUrl = aiUserAlbum.getImgUrl(); + //计算图片的失效时间从当前时间开始+12小时 + Long expTime = System.currentTimeMillis() + 12 * 60 * 60 * 1000L; + String ipAddress = input.getIpAddress(); + //如果是ipv4 则把日期和ip一起签名 + String img1Url = null; + String img2Url = null; + String img3Url = null; + try { + img1Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.IMG_468_600, new Date(expTime), ipAddress); + img2Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.IMG_800_800, new Date(expTime), ipAddress); + img3Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.ORG_IMG, new Date(expTime), ipAddress); + } catch (Exception e) { + log.error("viewUnlockAlbumImg 图片签名失败", e); + } + ViewUnlockAlbumImgOutput output = new ViewUnlockAlbumImgOutput(); + output.setImg1(img1Url); + output.setImg2(img2Url); + output.setImg3(img3Url); + return output; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/RankServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/RankServiceImpl.java new file mode 100644 index 0000000..e71eb70 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/RankServiceImpl.java @@ -0,0 +1,195 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Lists; +import com.sonic.frog.dao.AiUserStatDao; +import com.sonic.frog.domain.bo.AiGiftRankBo; +import com.sonic.frog.domain.bo.AiHeartbeatRankBo; +import com.sonic.frog.domain.bo.ChatRankBo; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.output.AiChatRankOutput; +import com.sonic.frog.domain.output.AiGiftRankOutput; +import com.sonic.frog.domain.output.AiHeartbeatRankOutput; +import com.sonic.frog.service.AiUserSearchService; +import com.sonic.frog.service.AiUserService; +import com.sonic.frog.service.RankService; +import com.sonic.frog.utils.BeanConvert; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class RankServiceImpl implements RankService { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private AiUserStatDao aiUserStatDao; + @Autowired + private AiUserService aiUserService; + @Autowired + private AiUserSearchService aiUserSearchService; + + @Override + public int chatRankJob() { + //统计榜单top 120 的数据 + List boList = aiUserStatDao.queryChatRank(120); + //将榜单数据写入redis缓存中,过期时间7天吧 + stringRedisTemplate.opsForValue().set(redisKeyUtils.chatRankKey(), JSONObject.toJSONString(boList), 7, TimeUnit.DAYS); + return boList.size(); + } + + @Override + public List chatRank(Long userId, Integer limit) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKeyUtils.chatRankKey()); + if(StringUtils.isEmpty(cacheStr)) { + return Collections.emptyList(); + } + //转换成列表对象 + List boList = JSONObject.parseArray(cacheStr, ChatRankBo.class); + if(CollectionUtils.isEmpty(boList)) { + return Collections.emptyList(); + } + List idList = boList.stream().map(ChatRankBo::getAiId).collect(Collectors.toList()); + Map chatNumMap = boList.stream().collect(Collectors.toMap(ChatRankBo::getAiId, ChatRankBo::getChatNum)); + Map likedNumMap = boList.stream().collect(Collectors.toMap(ChatRankBo::getAiId, ChatRankBo::getLikedNum)); + //获取AI基础信息 + List list = aiUserService.list(Wrappers.lambdaQuery().in(AiUser::getAiId, idList).eq(AiUser::getIsDelete, false)); + //排序,根据idList的顺序对list进行排序 + list.sort(Comparator.comparingInt(o -> idList.indexOf(o.getAiId()))); + //获取前100条数据 + list = list.subList(0, Math.min(list.size(), limit)); + //获取字典数据 + Map aiDictNameMap = aiUserSearchService.mapNameByCodeList(list); + //批量获取聊天次数 + List outputList = Lists.newArrayList(); + int rankNo = 1; + for (AiUser aiUser : list) { + AiChatRankOutput output = BeanConvert.copeBean(aiUser, AiChatRankOutput.class); + //组装字典数据 + output.setRoleName(aiDictNameMap.get(aiUser.getRoleCode())); + output.setCharacterName(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTagName(aiDictNameMap.get(aiUser.getTagCode())); + output.setChatNum(chatNumMap.get(aiUser.getAiId())); + output.setLikedNum(likedNumMap.get(aiUser.getAiId())); + output.setRankNo(rankNo++); + outputList.add(output); + } + return outputList; + } + + @Override + public int aiHeartbeatRankJob() { + //统计榜单top 120 的数据 + List boList = aiUserStatDao.queryAiHeartbeatRank(120); + //将榜单数据写入redis缓存中,过期时间7天吧 + stringRedisTemplate.opsForValue().set(redisKeyUtils.aiHeartbeatRankKey(), JSONObject.toJSONString(boList), 7, TimeUnit.DAYS); + return boList.size(); + } + + @Override + public List aiHeartbeatRank(Long userId, Integer limit) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKeyUtils.aiHeartbeatRankKey()); + if(StringUtils.isEmpty(cacheStr)) { + return Collections.emptyList(); + } + //转换成列表对象 + List boList = JSONObject.parseArray(cacheStr, AiHeartbeatRankBo.class); + if(CollectionUtils.isEmpty(boList)) { + return Collections.emptyList(); + } + List idList = boList.stream().map(AiHeartbeatRankBo::getAiId).collect(Collectors.toList()); + Map heartbeatValTotalMap = boList.stream().collect(Collectors.toMap(AiHeartbeatRankBo::getAiId, AiHeartbeatRankBo::getHeartbeatValTotal)); + Map likedNumMap = boList.stream().collect(Collectors.toMap(AiHeartbeatRankBo::getAiId, AiHeartbeatRankBo::getLikedNum)); + + //获取AI基础信息 + List list = aiUserService.list(Wrappers.lambdaQuery().in(AiUser::getAiId, idList).eq(AiUser::getIsDelete, false)); + //排序,根据idList的顺序对list进行排序 + list.sort(Comparator.comparingInt(o -> idList.indexOf(o.getAiId()))); + //获取前100条数据 + list = list.subList(0, Math.min(list.size(), limit)); + //获取字典数据 + Map aiDictNameMap = aiUserSearchService.mapNameByCodeList(list); + //批量获取聊天次数 + List outputList = Lists.newArrayList(); + int rankNo = 1; + for (AiUser aiUser : list) { + AiHeartbeatRankOutput output = BeanConvert.copeBean(aiUser, AiHeartbeatRankOutput.class); + //组装字典数据 + output.setRoleName(aiDictNameMap.get(aiUser.getRoleCode())); + output.setCharacterName(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTagName(aiDictNameMap.get(aiUser.getTagCode())); + output.setHeartbeatValTotal(heartbeatValTotalMap.get(aiUser.getAiId())); + output.setLikedNum(likedNumMap.get(aiUser.getAiId())); + output.setRankNo(rankNo++); + outputList.add(output); + } + return outputList; + } + + + @Override + public int giftRankJob() { + //统计榜单top 120 的数据 + List boList = aiUserStatDao.queryAiGiftRank(120); + //将榜单数据写入redis缓存中,过期时间7天吧 + stringRedisTemplate.opsForValue().set(redisKeyUtils.aiGiftRankKey(), JSONObject.toJSONString(boList), 7, TimeUnit.DAYS); + return boList.size(); + } + + @Override + public List giftRank(Long userId, Integer limit) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKeyUtils.aiGiftRankKey()); + if(StringUtils.isEmpty(cacheStr)) { + return Collections.emptyList(); + } + //转换成列表对象 + List boList = JSONObject.parseArray(cacheStr, AiGiftRankBo.class); + if(CollectionUtils.isEmpty(boList)) { + return Collections.emptyList(); + } + List idList = boList.stream().map(AiGiftRankBo::getAiId).collect(Collectors.toList()); + Map giftCoinNumMap = boList.stream().collect(Collectors.toMap(AiGiftRankBo::getAiId, AiGiftRankBo::getGiftCoinNum)); + Map likedNumMap = boList.stream().collect(Collectors.toMap(AiGiftRankBo::getAiId, AiGiftRankBo::getLikedNum)); + //获取AI基础信息 + List list = aiUserService.list(Wrappers.lambdaQuery().in(AiUser::getAiId, idList).eq(AiUser::getIsDelete, false)); + //排序,根据idList的顺序对list进行排序 + list.sort(Comparator.comparingInt(o -> idList.indexOf(o.getAiId()))); + //获取前100条数据 + list = list.subList(0, Math.min(list.size(), limit)); + //获取字典数据 + Map aiDictNameMap = aiUserSearchService.mapNameByCodeList(list); + //批量获取聊天次数 + List outputList = Lists.newArrayList(); + int rankNo = 1; + for (AiUser aiUser : list) { + AiGiftRankOutput output = BeanConvert.copeBean(aiUser, AiGiftRankOutput.class); + //组装字典数据 + output.setRoleName(aiDictNameMap.get(aiUser.getRoleCode())); + output.setCharacterName(aiDictNameMap.get(aiUser.getCharacterCode())); + output.setTagName(aiDictNameMap.get(aiUser.getTagCode())); + output.setGiftCoinNum(giftCoinNumMap.get(aiUser.getAiId())); + output.setLikedNum(likedNumMap.get(aiUser.getAiId())); + output.setRankNo(rankNo++); + outputList.add(output); + } + return outputList; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/SignInRecordServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/SignInRecordServiceImpl.java new file mode 100644 index 0000000..228ab1a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/SignInRecordServiceImpl.java @@ -0,0 +1,161 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.common.utils.RedisLock; +import com.sonic.frog.dao.SignInRecordDao; +import com.sonic.frog.domain.entity.SignInRecord; +import com.sonic.frog.domain.entity.SignInStat; +import com.sonic.frog.service.SignInRecordService; +import com.sonic.frog.service.SignInStatService; +import com.sonic.frog.utils.DateConvertUtils; +import com.sonic.frog.utils.RedisKeyUtils; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.input.PlatformGiftInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * 签到明细表 + * @author zzhan + */ +@Slf4j +@Service +public class SignInRecordServiceImpl extends ServiceImpl implements SignInRecordService { + + @Autowired + private SignInStatService signInStatService; + + @Autowired + private RedisLock.RedisWrapper redisWrapper; + + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private PayClient payClient; + + @Transactional(rollbackFor = Exception.class) + @Override + public Boolean signIn(Long currentUserId) { + AtomicBoolean atomicBoolean = new AtomicBoolean(false); + //加锁处理 + RedisLock redisLock = new RedisLock(redisKeyUtils.signInLockKey(currentUserId), redisWrapper); + redisLock.tryAcquireRun(30 * 1000L, () -> { + atomicBoolean.set(signInHandler(currentUserId)); + //判断是否签到成功 + if(atomicBoolean.get()) { + //签到成功,给用户发放签到奖励 coin + PlatformGiftInput platformGiftInput = new PlatformGiftInput(); + platformGiftInput.setPlatform("1"); + platformGiftInput.setUid(currentUserId); + platformGiftInput.setName(PlatformGiftInput.Type.SIGN_IN_GIFT.getBizType().getDesc()); + platformGiftInput.setCreateTime(LocalDateTime.now()); + platformGiftInput.setAmount(500L); + platformGiftInput.setType(PlatformGiftInput.Type.SIGN_IN_GIFT); + payClient.platformGift(platformGiftInput); + } + return true; + }); + return atomicBoolean.get(); + } + + @Override + public SignInStat init(Long currentUserId) { + LocalDateTime pstNowTime = DateConvertUtils.getDateLaTime(); + return init(currentUserId, pstNowTime); + } + + /** + * 签到具体业务处理逻辑 + * @param currentUserId + */ + private boolean signInHandler(Long currentUserId) { + LocalDateTime pstNowTime = DateConvertUtils.getDateLaTime(); + //看当前PST时间是否在这个时间段内 + String currentDay = DateConvertUtils.formatYearMonthDay(pstNowTime); + //处理数据初始化 + SignInStat signInStat = init(currentUserId, pstNowTime); + + //写入当前签到数据,并且统计连续签到天数,然后更新到统计表中 + boolean saveBl = saveSignInRecord(currentUserId, pstNowTime); + if(!saveBl) { + //保存数据出错,直接快速返回 + return false; + } + //统计签到总天数 + int count = count(Wrappers.lambdaQuery() + .eq(SignInRecord::getUserId, currentUserId) + .ge(SignInRecord::getDayStr, signInStat.getStartDay()) + .le(SignInRecord::getDayStr, currentDay)); + //更新连续签到天数 + signInStatService.updateStat(signInStat.getId(), count); + return true; + } + + /** + * 签到周期数据初始化 + * @param currentUserId + * @param pstNowTime + * @return + */ + private SignInStat init(Long currentUserId, LocalDateTime pstNowTime) { + //查询统计表的基础数据 + SignInStat signInStat = signInStatService.get(currentUserId); + //没有数据的话需要初始化一条数据进去,写入签到的周期从今天开始 + if(signInStat == null) { + signInStat = signInStatService.init(currentUserId); + } + //判断结束时间是否在当前时间之前,如果是的话需要重新初始化周期数据 + if(DateConvertUtils.convertStringToDateTime(signInStat.getEndDay()).isBefore(DateConvertUtils.getDateMinTime())) { + signInStat.setStartDay(DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime())); + signInStat.setEndDay(DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime().plusDays(6))); + //重新初始化周期,今天为开始日期 + signInStatService.updateStartEndDayAndDays(signInStat.getId(), signInStat.getStartDay(), signInStat.getEndDay()); + } + //获取起止日期 + String startDay = signInStat.getStartDay(); + //如果今天是第一天开始时间则不用判断昨天是否签到,否则需要进行判断 + if(DateConvertUtils.convertStringToDateTime(startDay).isBefore(pstNowTime.minusDays(1))) { + //昨天漏了一天的情况,今天的话是不算的,所以需要查询昨天的签到数据是否存在 + SignInRecord yesterdaySignInRecord = getOne(Wrappers.lambdaQuery() + .select(SignInRecord::getId, SignInRecord::getDayStr) + .eq(SignInRecord::getUserId, currentUserId) + .eq(SignInRecord::getDayStr, DateConvertUtils.formatYearMonthDay(pstNowTime.minusDays(1)))); + if(yesterdaySignInRecord == null) { + //重新初始化数据 + signInStat.setStartDay(DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime())); + signInStat.setEndDay(DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime().plusDays(6))); + //重新初始化周期,今天为开始日期 + signInStatService.updateStartEndDayAndDays(signInStat.getId(), signInStat.getStartDay(), signInStat.getEndDay()); + } + } + return signInStat; + } + + + /** + * 保存签到明细数据 + * @param currentUserId + * @param pstNowTime + * @return + */ + private boolean saveSignInRecord(Long currentUserId, LocalDateTime pstNowTime) { + try { + SignInRecord signInRecord = new SignInRecord(); + signInRecord.setUserId(currentUserId); + signInRecord.setDayStr(DateConvertUtils.formatYearMonthDay(pstNowTime)); + signInRecord.setCreateTime(LocalDateTime.now()); + save(signInRecord); + return true; + } catch (Exception e) { + //触发唯一所以得数据写入操作,直接快速返回掉 + return false; + } + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/SignInStatSearchServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/SignInStatSearchServiceImpl.java new file mode 100644 index 0000000..dad2309 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/SignInStatSearchServiceImpl.java @@ -0,0 +1,131 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Lists; +import com.sonic.frog.dao.SignInStatDao; +import com.sonic.frog.domain.entity.SignInRecord; +import com.sonic.frog.domain.entity.SignInStat; +import com.sonic.frog.domain.output.SignInListOutput; +import com.sonic.frog.domain.output.SignInRoundOutput; +import com.sonic.frog.enums.SignInCoinNumEnum; +import com.sonic.frog.service.SignInRecordService; +import com.sonic.frog.service.SignInStatSearchService; +import com.sonic.frog.service.SignInStatService; +import com.sonic.frog.utils.DateConvertUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @Author zzhan + * @Description 签到统计数据表查询 + * @Date 2024/6/11 11:13 + * @Version 1.0 + */ +@Slf4j +@Service +public class SignInStatSearchServiceImpl implements SignInStatSearchService { + + @Autowired + private SignInStatDao signInStatDao; + @Autowired + private SignInStatService signInStatService; + @Autowired + private SignInRecordService signInRecordService; + + @Override + public SignInRoundOutput signInList(Long currentUserId) { + SignInRoundOutput output = new SignInRoundOutput(); + //获取并初始化签到周期数据 + SignInStat signInStat = getAndInitSignInStat(currentUserId); + //查询签到明细数据 + List list = signInRecordService.list(Wrappers.lambdaQuery() + .select(SignInRecord::getDayStr) + .eq(SignInRecord::getUserId, currentUserId)); + Map recordMap = list.stream().collect(Collectors.toMap(e -> e.getDayStr(), Function.identity())); + //获取时间周期内的所有日期 + List dayStrList = DateConvertUtils.getDateStringsBetween(signInStat.getStartDay(), signInStat.getEndDay()); + List outputList = Lists.newArrayList(); + int dayNum = 0; + //循环日期 组装完整的7天签到列表数据返回给前端 + for (String dayStr : dayStrList) { + dayNum = dayNum + 1; + //获取coinNum + SignInListOutput signInListOutput = new SignInListOutput(); + signInListOutput.setDayStr(dayStr); + SignInRecord signInRecord = recordMap.get(dayStr); + signInListOutput.setSignIn(signInRecord != null); + signInListOutput.setCoinNum(SignInCoinNumEnum.getCoinNum(dayNum)); + outputList.add(signInListOutput); + } + output.setList(outputList); + //返回最大连续签到天数 + output.setContinuousDays(signInStat.getAllDays()); + return output; + } + + /** + * 获取并初始化签到周期数据 + * @param currentUserId + * @return + */ + private SignInStat getAndInitSignInStat(Long currentUserId) { + SignInStat signInStat = signInStatService.getOne(Wrappers.lambdaQuery() + .eq(SignInStat::getUserId, currentUserId) + .orderByDesc(SignInStat::getId) + .last("limit 1")); + //今天的日期 + String todayStr = DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime()); + boolean queryDataBl = false; + //判断当前时间是否已超过周期 + if(signInStat == null || !DateConvertUtils.currentDayInRangeDayCheck(todayStr, signInStat.getStartDay(), signInStat.getEndDay())) { + //初始化签到周期数据 + if(signInStat == null) { + signInStatService.init(currentUserId); + } else { + //更新数据 + signInStat.setStartDay(DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime())); + signInStat.setEndDay(DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime().plusDays(6))); + //重新初始化周期,今天为开始日期 + signInStatService.updateStartEndDayAndDays(signInStat.getId(), signInStat.getStartDay(), signInStat.getEndDay()); + } + //需要重新查询数据 + queryDataBl = true; + } else { + //获取系统当前时间 + LocalDateTime pstNowTime = DateConvertUtils.getDateLaTime(); + //如果今天是第一天开始时间则不用判断昨天是否签到,否则需要进行判断 + if(DateConvertUtils.convertStringToDateTime(signInStat.getStartDay()).isBefore(pstNowTime.minusDays(1))) { + //昨天漏了一天的情况,今天的话是不算的,所以需要查询昨天的签到数据是否存在 + SignInRecord yesterdaySignInRecord = signInRecordService.getOne(Wrappers.lambdaQuery() + .select(SignInRecord::getId, SignInRecord::getDayStr) + .eq(SignInRecord::getUserId, currentUserId) + .eq(SignInRecord::getDayStr, DateConvertUtils.formatYearMonthDay(pstNowTime.minusDays(1)))); + if(yesterdaySignInRecord == null) { + //重新初始化数据 + signInStat.setStartDay(DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime())); + signInStat.setEndDay(DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime().plusDays(6))); + //重新初始化周期,今天为开始日期 + signInStatService.updateStartEndDayAndDays(signInStat.getId(), signInStat.getStartDay(), signInStat.getEndDay()); + //需要重新查询数据 + queryDataBl = true; + } + } + } + if(queryDataBl) { + //初始化完了后重新查询一下数据 + signInStat = signInStatService.getOne(Wrappers.lambdaQuery() + .eq(SignInStat::getUserId, currentUserId) + .orderByDesc(SignInStat::getId) + .last("limit 1")); + } + return signInStat; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/SignInStatServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/SignInStatServiceImpl.java new file mode 100644 index 0000000..72e876b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/SignInStatServiceImpl.java @@ -0,0 +1,70 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.SignInStatDao; +import com.sonic.frog.domain.entity.SignInStat; +import com.sonic.frog.service.SignInStatService; +import com.sonic.frog.utils.DateConvertUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +/** + * 签到统计数据表 + * @author zzhan + */ +@Slf4j +@Service +public class SignInStatServiceImpl extends ServiceImpl implements SignInStatService { + + @Autowired + private SignInStatDao signInStatDao; + + @Override + public SignInStat get(Long currentUserId) { + SignInStat signInStat = getOne(Wrappers.lambdaQuery() + .eq(SignInStat::getUserId, currentUserId)); + return signInStat; + } + + @Override + public SignInStat init(Long currentUserId) { + SignInStat signInStat = null; + try { + signInStat = new SignInStat(); + signInStat.setUserId(currentUserId); + //获取PST时间的yyyy-MM-dd 七天的起止时间 + signInStat.setStartDay(DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime())); + signInStat.setEndDay(DateConvertUtils.formatYearMonthDay(DateConvertUtils.getDateLaTime().plusDays(6))); + signInStat.setAllDays(0); + signInStat.setCreateTime(LocalDateTime.now()); + signInStat.setEditTime(LocalDateTime.now()); + //保存到数据库中 + save(signInStat); + } catch (Exception e) { + log.error("SignInStatServiceImpl init:", e); + //查询数据并返回 + signInStat = getOne(Wrappers.lambdaQuery().eq(SignInStat::getUserId, currentUserId)); + } + return signInStat; + } + + @Override + public void updateStartEndDayAndDays(Long id, String startDay, String endDay) { + update(Wrappers.lambdaUpdate() + .set(SignInStat::getStartDay, startDay) + .set(SignInStat::getEndDay, endDay) + .eq(SignInStat::getId, id)); + } + + @Override + public void updateStat(Long id, Integer days) { + update(Wrappers.lambdaUpdate() + .set(days != null, SignInStat::getAllDays, days) + .eq(SignInStat::getId, id)); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ThirdLoginOrRegisterServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ThirdLoginOrRegisterServiceImpl.java new file mode 100644 index 0000000..2add28a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/ThirdLoginOrRegisterServiceImpl.java @@ -0,0 +1,79 @@ +package com.sonic.frog.service.impl; + +import com.sonic.bear.lib.client.UserLoginClient; +import com.sonic.bear.lib.enums.ThirdTypeEnum; +import com.sonic.bear.lib.input.ThirdUserLoginInput; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.exception.BizException; +import com.sonic.common.exception.BizExceptionUtils; +import com.sonic.common.rpc.GlobalResultCode; +import com.sonic.frog.client.AppleIdLoginValidateOauth2Client; +import com.sonic.frog.client.DiscordOauth2Client; +import com.sonic.frog.client.GoogleOauth2Client; +import com.sonic.frog.domain.bo.ThirdAuthBo; +import com.sonic.frog.domain.input.ThirdLoginOrRegisterInput; +import com.sonic.frog.domain.output.ThirdLoginOrRegisterOutput; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.service.ThirdLoginOrRegisterService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * @Author code + * @Description 登录或注册验证 + * @Version 1.0 + */ +@Slf4j +@Service +public class ThirdLoginOrRegisterServiceImpl implements ThirdLoginOrRegisterService { + + @Autowired + private GoogleOauth2Client googleOauth2Client; + @Autowired + private AppleIdLoginValidateOauth2Client appleIdLoginValidateOauth2Client; + @Autowired + private DiscordOauth2Client discordOauth2Client; + @Autowired + private UserLoginClient userLoginClient; + + @Override + public ThirdLoginOrRegisterOutput loginOrRegister(ThirdLoginOrRegisterInput input) { + //三方账号不管登录环境发生变更的问题,所以这里不做设备相关的校验 + ThirdAuthBo thirdAuthBo; + ThirdTypeEnum thirdType = input.getThirdType(); + switch (thirdType) { + case GOOGLE: + thirdAuthBo = googleOauth2Client.getGoogleIdByToken(input.getThirdToken()); + break; + case APPLE: + boolean valid = appleIdLoginValidateOauth2Client.isValid(input.getThirdToken(), input.getAppClient()); + BizExceptionUtils.check(!valid, "-1", "Apple account could not be verified"); + thirdAuthBo = appleIdLoginValidateOauth2Client.parseIdByIdToken(input.getThirdToken()); + break; + case DISCORD: + thirdAuthBo = discordOauth2Client.getDiscordIdByToken(input.getThirdToken()); + break; + default: + throw new BizException(GlobalResultCode.INVALID_PARAMS); + } + ToastResultCode.ACCOUNT_DOES_NOT_EXIST.check(StringUtils.isEmpty(thirdAuthBo.getThirdId())); + //执行注册并登录的操作 + ThirdUserLoginInput thirdUserLoginInput = ThirdUserLoginInput.builder() + .openId(thirdAuthBo.getThirdId()) + .email(thirdAuthBo.getEmail()) + .nickname(thirdAuthBo.getNickname()) + .thirdType(thirdType) + .clientCode(input.getAppClient()) + .deviceId(input.getDeviceCode()) + .ip(input.getIp()) + .userAgent(input.getUserAgent()).build(); + Session session = userLoginClient.thirdLogin(thirdUserLoginInput); + //构造出参对象 + ThirdLoginOrRegisterOutput output = new ThirdLoginOrRegisterOutput(); + output.setToken(session.getToken()); + return output; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/TimbreDictServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/TimbreDictServiceImpl.java new file mode 100644 index 0000000..a19b359 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/TimbreDictServiceImpl.java @@ -0,0 +1,43 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.TimbreDictDao; +import com.sonic.frog.domain.entity.TimbreDict; +import com.sonic.frog.service.TimbreDictService; +import com.sonic.frog.utils.CacheUtils; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 音色字典表服务实现类 + */ +@Service +@Slf4j +public class TimbreDictServiceImpl extends ServiceImpl implements TimbreDictService { + @Autowired + private RedisKeyUtils redisKeyUtils; + + @Autowired + private CacheUtils cacheUtils; + + @Override + public List getAllTimbreDictList() { + String aiTimbreDictCacheKey = redisKeyUtils.aiTimbreDictCacheKey(); + return cacheUtils.getCacheListAndSet(aiTimbreDictCacheKey, TimbreDict.class, () -> { + List list = list(Wrappers.lambdaQuery().eq(TimbreDict::getIsDelete, false)); + return JSON.toJSONString(list); + }, 1 * 60 * 60); + } + + @Override + public TimbreDict getTimbreDictByCode(String code) { + List allTimbreDictList = getAllTimbreDictList(); + return allTimbreDictList.stream().filter(timbreDict -> timbreDict.getCode().equals(code)).findFirst().orElse(null); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserAiMeetServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserAiMeetServiceImpl.java new file mode 100644 index 0000000..0d60e43 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserAiMeetServiceImpl.java @@ -0,0 +1,268 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.sonic.frog.dao.LikedDao; +import com.sonic.frog.dao.UserAiMeetDao; +import com.sonic.frog.domain.bo.RandomMeetRateBo; +import com.sonic.frog.domain.entity.*; +import com.sonic.frog.domain.output.MeetSdOutput; +import com.sonic.frog.event.inner.payload.AiUserStatPayload; +import com.sonic.frog.service.*; +import com.sonic.frog.utils.LimitUtils; +import com.sonic.frog.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + *

+ * 用户和AI相互喜欢记录表 + *

+ * + * @author mzc + * @since 2024-11-28 + */ +@Service +@Slf4j +public class UserAiMeetServiceImpl extends ServiceImpl implements UserAiMeetService { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private AiUserService aiUserService; + @Autowired + private LimitUtils limitUtils; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private LikedDao likedDao; + + /** + * 默认次数列表 + */ + List DEFAULT_INDEX_LIST = Lists.newArrayList( + 31, 62, 93, 114, 145, 176, 207, 238, 269, 300, + 331, 362, 393, 424, 455, 486, 517, 548, 579, 610 + ); + @Autowired + private AiUserStatService aiUserStatService; + @Autowired + private AiUserAlbumService aiUserAlbumService; + + @Override + public MeetSdOutput meetSd(Long userId, Long aiId, boolean lk) { + MeetSdOutput output = new MeetSdOutput(); + //先增加滑动次数 + int sdCount = limitUtils.defaultLimitCheckReturnCount(redisKeyUtils.meetSdCount(userId), 2 * 60 * 60); + //次数不足10次或已经大于70次了,或者不为喜欢的卡片,则直接快速返回掉 + if(sdCount < 10 || sdCount > 70 || !lk) { + output.setBd(false); + } else { + output.setBd(getLikeMeetBl(userId, aiId, sdCount)); + } + //计算是否触发秘密爱慕者 + if(DEFAULT_INDEX_LIST.contains(sdCount)) { + output.setRc(true); + } + if(userId != null && sdCount < 500) { + //点赞或取消点赞 + likedDao.likeOrCancel(Liked.builder() + .bizId(aiId) + .bizType(Liked.BizType.AI) + .aiId(aiId) + .likedUserId(userId) + .likedStatus(lk? Liked.LikedStatus.LIKED : Liked.LikedStatus.CANCELED).build()); + //如果是喜欢或者不喜欢的AI的话,需要发送MQ消息,异步计算喜欢总数 + commonSendMqService.aiUserStatMq(aiId, lk ? AiUserStatPayload.Type.LIKED : AiUserStatPayload.Type.CANCEL_LIKED, null); + } + return output; + } + + /** + * 获取当前喜欢是否匹配上了 + * @param userId + * @param aiId + * @param sdCount + * @return + */ + private boolean getLikeMeetBl(Long userId, Long aiId, int sdCount) { + RandomMeetRateBo bo = getRateBo(userId); + if(bo == null) { + return false; + } + if(sdCount >= 11 && sdCount <= 20 && bo.getBl1120()) { + //写入待消费数据、10秒钟过期,并修改当前的状态 + bo.setBl1120(false); + updateRateBoAndSetUploadAccess(userId, aiId, bo); + return true; + } + if(sdCount >= 31 && sdCount <= 40 && (bo.getBl1120() || bo.getBl3140())) { + //写入待消费数据、10秒钟过期,并修改当前的状态 + bo.setBl1120(false); + bo.setBl3140(false); + updateRateBoAndSetUploadAccess(userId, aiId, bo); + return true; + } + if(sdCount >= 41 && sdCount <= 50 && (bo.getBl1120() || bo.getBl4150())) { + //写入待消费数据、10秒钟过期,并修改当前的状态 + bo.setBl1120(false); + bo.setBl3140(false); + bo.setBl4150(false); + updateRateBoAndSetUploadAccess(userId, aiId, bo); + return true; + } + if(sdCount >= 51 && sdCount <= 60 && (bo.getBl1120() || bo.getBl5160())) { + //写入待消费数据、10秒钟过期,并修改当前的状态 + bo.setBl1120(false); + bo.setBl3140(false); + bo.setBl4150(false); + bo.setBl5160(false); + updateRateBoAndSetUploadAccess(userId, aiId, bo); + return true; + } + if(sdCount >= 61 && sdCount <= 70 && (bo.getBl1120() || bo.getBl6170())) { + //写入待消费数据、10秒钟过期,并修改当前的状态 + bo.setBl1120(false); + bo.setBl3140(false); + bo.setBl4150(false); + bo.setBl5160(false); + bo.setBl6170(false); + updateRateBoAndSetUploadAccess(userId, aiId, bo); + return true; + } + return false; + } + + /** + * 获取随机匹配的概率 + * @param userId + * @return + */ + private RandomMeetRateBo getRateBo(Long userId) { + //获取滑动的 + String meetRateStr = stringRedisTemplate.opsForValue().get(redisKeyUtils.meetRateBoKey(userId)); + if(StringUtils.isEmpty(meetRateStr)) { + return null; + } + log.info("===> getRateBo : {}", meetRateStr); + return JSONObject.parseObject(meetRateStr, RandomMeetRateBo.class); + } + + /** + * 修改值,并设置上报meet权限 + * @param userId + * @param bo + */ + private void updateRateBoAndSetUploadAccess(Long userId, Long aiId, RandomMeetRateBo bo) { + //写入redis,赋予上传权限 + stringRedisTemplate.opsForValue().set(redisKeyUtils.uploadMeet(userId, aiId), "1", 60, TimeUnit.SECONDS); + //获取过期时间 + long expTime = stringRedisTemplate.getExpire(redisKeyUtils.meetRateBoKey(userId)); + log.debug("===> updateRateBoAndSetUploadAccess expTime : {}", expTime); + if(expTime <= 0) { + //如果是永不过期,那么删掉数据 + if(expTime == -1) { + stringRedisTemplate.delete(redisKeyUtils.meetRateBoKey(userId)); + } + return; + } + String redisValue = JSONObject.toJSONString(bo); + log.info("===> updateRateBoAndSetUploadAccess redisValue : {}", redisValue); + stringRedisTemplate.opsForValue().set(redisKeyUtils.meetRateBoKey(userId), redisValue, expTime, TimeUnit.SECONDS); + } + + + @Override + public boolean meetBd(Long userId, Long aiId) { + boolean hasKey = stringRedisTemplate.hasKey(redisKeyUtils.uploadMeet(userId, aiId)); + //校验用户是否有上报meet卡片的权限,没有的话直接快速返回掉,不进行任何处理 + if(!hasKey) { + return false; + } + //判断AI是否存在 且 不能是自己的AI,是否公开 + int count = aiUserService.count(Wrappers.lambdaQuery() + .eq(AiUser::getAiId, aiId) + .ne(AiUser::getUserId, userId) + .eq(AiUser::getIsDelete, false) + .eq(AiUser::getPermission, 1)); + if(count != 1) { + return false; + } + //执行添加操作是否成功 + boolean add = addMeet(userId, aiId); + if(add) { + //TODO 绑定成功,发送IM消息??? + + } + return add; + } + + @Override + public Long meetRc(Long userId) { + //频率限制,2小时内同一个用户最多调用20次,超过20次则直接不返回任何数据 + int limit = limitUtils.defaultLimitCheckReturnCount(redisKeyUtils.meetRcLimit(userId), 2 * 60 * 60); + if(limit > 20) { + return null; + } + //查询待推荐数据:从被喜欢次数排名前100的随机给出一个 + List exAiIds = list(Wrappers.lambdaQuery() + .select(UserAiMeet::getAiId) + .eq(UserAiMeet::getUserId, userId)).stream().map(UserAiMeet::getAiId) + .collect(Collectors.toList()); + //排除掉已喜欢的数据 + List aiIdList = aiUserStatService.list(Wrappers.lambdaQuery() + .select(AiUserStat::getAiId) + .notIn(CollectionUtils.isNotEmpty(exAiIds), AiUserStat::getAiId, exAiIds) + .orderByDesc(AiUserStat::getLikedNum).last("LIMIT 50")) + .stream().map(AiUserStat::getAiId).collect(Collectors.toList()); + if(CollectionUtils.isEmpty(aiIdList)) { + return null; + } + //查询默认图片ID出来 + List aiUserAlbumList = aiUserAlbumService.list(Wrappers.lambdaQuery() + .select(AiUserAlbum::getId) + .in(AiUserAlbum::getAiId, aiIdList) + .eq(AiUserAlbum::getIsDefault, true) + .eq(AiUserAlbum::getIsDelete, false)); + if(CollectionUtils.isEmpty(aiUserAlbumList)) { + return null; + } + //随机 + Collections.shuffle(aiUserAlbumList); + //获取一条数据 + AiUserAlbum aiUserAlbum = aiUserAlbumList.get(0); + return aiUserAlbum == null ? null : aiUserAlbum.getId(); + } + + @Override + public boolean addMeet(Long userId, Long aiId) { + try { + UserAiMeet userAiMeet = UserAiMeet.builder() + .userId(userId) + .aiId(aiId) + .createTime(LocalDateTime.now()) + .build(); + save(userAiMeet); + } catch (Exception e) { + //吃掉异常,返回失败 + return false; + } + return true; + } + +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserCreateCountStatServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserCreateCountStatServiceImpl.java new file mode 100644 index 0000000..1ba740a --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserCreateCountStatServiceImpl.java @@ -0,0 +1,137 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.frog.dao.UserCreateCountStatDao; +import com.sonic.frog.domain.entity.UserCreateCountStat; +import com.sonic.frog.domain.output.UserCreateCountOutput; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.service.UserCreateCountStatService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 用户相册创作次数统计 Service实现类 + */ +@Slf4j +@Service +public class UserCreateCountStatServiceImpl extends ServiceImpl implements UserCreateCountStatService { + + @Autowired + private UserCreateCountStatDao userCreateCountStatDao; + + /** + * 免费创建图片数量 + */ + private final static Integer FREE_NUM = 10; + + /** + * 会员赠送创建图片数量 + */ + private final static Integer MEMBER_NUM = 10; + + /** + * 会员赠送次数周期天数,每隔30天赠送一次 + */ + private final static Integer MEMBER_GIFT_DAY = 30; + + @Override + public void addBuyNum(Long userId, Integer count) { + userCreateCountStatDao.addBuyNum(userId, count); + } + + @Override + public void subMemberGiftUserCreateCount(Long userId, LocalDateTime startTime, LocalDateTime expireTime) { + UserCreateCountStat userCreateCountStat = initUserCreateCountStat(userId); + if (userCreateCountStat.getMemberExpTime() == null) { + //首次成为会员 + userCreateCountStat.setMemberNum(MEMBER_NUM); + //加30天,记录下次赠送时间 + userCreateCountStat.setNextGiftTime(startTime.plusDays(MEMBER_GIFT_DAY)); + //记录会员过期时间 + userCreateCountStat.setMemberExpTime(expireTime); + } else { + if (startTime.isAfter(userCreateCountStat.getMemberExpTime())) { + //过期后订阅 + userCreateCountStat.setMemberNum(userCreateCountStat.getMemberNum() + MEMBER_NUM); + userCreateCountStat.setNextGiftTime(startTime.plusDays(MEMBER_GIFT_DAY)); + userCreateCountStat.setMemberExpTime(expireTime); + } else { + //有效期内续订,只更新会员过期时间就行了 + userCreateCountStat.setMemberExpTime(expireTime); + } + } + //更新数据库 + updateById(userCreateCountStat); + } + + private UserCreateCountStat initUserCreateCountStat(Long userId) { + UserCreateCountStat userCreateCountStat = getOne(Wrappers.lambdaQuery().eq(UserCreateCountStat::getUserId, userId).last("limit 1")); + if (userCreateCountStat == null) { + userCreateCountStat = new UserCreateCountStat(); + userCreateCountStat.setUserId(userId); + userCreateCountStat.setFreeNum(FREE_NUM); + userCreateCountStat.setUsedFreeNum(0); + userCreateCountStat.setMemberNum(0); + userCreateCountStat.setUsedMemberNum(0); + userCreateCountStat.setBuyNum(0); + userCreateCountStat.setUsedBuyNum(0); + userCreateCountStat.setCreateTime(LocalDateTime.now()); + userCreateCountStat.setEditTime(LocalDateTime.now()); + save(userCreateCountStat); + } + return userCreateCountStat; + } + + @Override + public UserCreateCountOutput getUserCreateCount(Long userId) { + //查询用户创作次数,没有的话,初始化 + UserCreateCountStat userCreateCountStat = initUserCreateCountStat(userId); + return UserCreateCountOutput.builder().freeNum(userCreateCountStat.getFreeNum()).usedFreeNum(userCreateCountStat.getUsedFreeNum()).memberNum(userCreateCountStat.getMemberNum()).usedMemberNum(userCreateCountStat.getUsedMemberNum()).buyNum(userCreateCountStat.getBuyNum()).usedBuyNum(userCreateCountStat.getUsedBuyNum()).build(); + } + + @Override + public void useUserCreateCount(Long userId) { + UserCreateCountStat userCreateCountStat = getOne(Wrappers.lambdaQuery().eq(UserCreateCountStat::getUserId, userId).last("limit 1")); + ToastResultCode.USER_CREATE_COUNT_NONE.check(userCreateCountStat == null); + if (userCreateCountStat.getFreeNum() > 0 && userCreateCountStat.getUsedFreeNum() < userCreateCountStat.getFreeNum()) { + //免费次数还有,已使用+1 + userCreateCountStat.setUsedFreeNum(userCreateCountStat.getUsedFreeNum() + 1); + } else if (userCreateCountStat.getMemberNum() > 0 && userCreateCountStat.getUsedMemberNum() < userCreateCountStat.getMemberNum()) { + //会员赠送次数还有,已使用+1 + userCreateCountStat.setUsedMemberNum(userCreateCountStat.getUsedMemberNum() + 1); + } else if (userCreateCountStat.getBuyNum() > 0 && userCreateCountStat.getUsedBuyNum() < userCreateCountStat.getBuyNum()) { + //购买次数还有,已使用+1 + userCreateCountStat.setUsedBuyNum(userCreateCountStat.getUsedBuyNum() + 1); + } else { + ToastResultCode.USER_CREATE_COUNT_NONE.check(true); + } + //更新数据库 + update(userCreateCountStat, Wrappers.lambdaUpdate().eq(UserCreateCountStat::getId, userCreateCountStat.getId())); + } + + @Override + public void giftMemberNumJob() { + //获取待赠送会员次数的列表 + List userCreateCountStatList = userCreateCountStatDao.getWaitingGiftMemberNumList(); + userCreateCountStatList.forEach(userCreateCountStat -> { + if (userCreateCountStat.getNextGiftTime().isBefore(LocalDateTime.now())) { + //如果下次赠送时间在会员过期时间之后,则下次赠送时间置空 + LocalDateTime nextGiftTime = userCreateCountStat.getNextGiftTime().plusDays(MEMBER_GIFT_DAY); + //加5分钟,防止最后一次又赠送 + if (nextGiftTime.plusMinutes(5).isAfter(userCreateCountStat.getMemberExpTime())) { + nextGiftTime = null; + } + //更新数据库 + update(Wrappers.lambdaUpdate() + .set(UserCreateCountStat::getMemberNum, userCreateCountStat.getMemberNum() + MEMBER_NUM) + .set(UserCreateCountStat::getNextGiftTime, nextGiftTime) + .eq(UserCreateCountStat::getId, userCreateCountStat.getId())); + } + }); + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserDeductionStatServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserDeductionStatServiceImpl.java new file mode 100644 index 0000000..8c98376 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserDeductionStatServiceImpl.java @@ -0,0 +1,225 @@ +package com.sonic.frog.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.sonic.frog.dao.UserDeductionStatDao; +import com.sonic.frog.domain.entity.UserDeductionStat; +import com.sonic.frog.enums.DeductionTypeEnum; +import com.sonic.frog.service.UserDeductionStatService; +import com.sonic.frog.utils.KeyGenerator; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.enums.BizType; +import com.sonic.lion.lib.input.BalanceCheckoutInput; +import com.sonic.lion.lib.output.AccountBuffOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 用户聊天、语音,语音通话扣费统计表 Service实现类 + *

+ * *文本消息(含聊天辅助) + * 发送一条,计入预扣费总金额,同时检查用户帐户余额是否足够, + * 余额够时:每一次发送文本10分钟后,如果没有新的消息产生,计入一条流水 + * 余额不够时:余额减预扣费金额,算一次流水。 + *

+ * *发送和听取语音 + * 每一次发送或听取计入预扣费总金额,同时检查用户帐户余额是否足够。 + * 余额够时:每一次听取和发送后语音消息10分钟后,如果没有新的消息产生,计入一条流水 + * 余额不够时:余额减预扣费金额,算一次流水。 + *

+ * *语音通话收费 + * 每一分钟,计入预扣费总金额,同时检查用户帐户余额是否足够。 + * 余额够时:在每一次语音通话结束后即可生成流水。 + * 余额不够时:余额减预扣费金额直接生成流水。 + */ +@Slf4j +@Service +public class UserDeductionStatServiceImpl extends ServiceImpl implements UserDeductionStatService { + + @Autowired + private PayClient payClient; + @Autowired + private UserDeductionStatDao userDeductionStatDao; + + @Override + public void userDeductionStat(Long userId, Long aiId, DeductionTypeEnum deductionType, String extra) { + //查询统计是否存在 + UserDeductionStat userDeductionStat = getOne(Wrappers.lambdaQuery() + .eq(UserDeductionStat::getUserId, userId) + .eq(UserDeductionStat::getAiId, aiId) + .eq(UserDeductionStat::getDeductionType, deductionType.getIndex()) + .last("limit 1") + ); + //如果是语音通话且通话已经结束,则生成流水 + if (DeductionTypeEnum.VOICE_CALL.equals(deductionType)) { + Map map = JSONObject.parseObject(extra, Map.class); + int status = Integer.parseInt(map.get("status").toString()); + //通话结束 + if (status == 3) { + //为什么要大于0才执行,因为在余额不足时,会生成流水,并置为0 + if (userDeductionStat != null && userDeductionStat.getPreDeductionAmount() > 0) { + //发起支付扣款 + saveUserBill(userId, deductionType, userDeductionStat.getPreDeductionAmount()); + //预扣费金额置为0 + updatePreDeductionAmountToZero(userDeductionStat.getId()); + } + //结束后直接return + return; + } + } + + //总的预扣金额 + Long totalPreDeductionAmount = deductionType.getAmount(); + if (userDeductionStat != null) { + totalPreDeductionAmount = userDeductionStat.getPreDeductionAmount() + deductionType.getAmount(); + } + + if (userDeductionStat == null) { + //不存在,初始化 + userDeductionStat = new UserDeductionStat(); + userDeductionStat.setUserId(userId); + userDeductionStat.setAiId(aiId); + userDeductionStat.setDeductionType(deductionType.getIndex()); + userDeductionStat.setPreDeductionAmount(deductionType.getAmount()); + userDeductionStat.setLastTime(LocalDateTime.now()); + userDeductionStat.setCreateTime(LocalDateTime.now()); + userDeductionStat.setEditTime(LocalDateTime.now()); + save(userDeductionStat); + } else { + //更新总的预扣金额 + update(Wrappers.lambdaUpdate() + .eq(UserDeductionStat::getId, userDeductionStat.getId()) + //更新预扣费金额+本次预扣费金额 + .set(UserDeductionStat::getPreDeductionAmount, totalPreDeductionAmount) + .set(UserDeductionStat::getLastTime, LocalDateTime.now()) + .set(UserDeductionStat::getEditTime, LocalDateTime.now()) + ); + } + + //每一次都要扣款结账但不生成流水(扣除用户余额) + checkoutToPlatformNotBill(userId, deductionType, deductionType.getAmount()); + } + + /** + * 扣款但不生成流水 + * + * @param userId + * @param deductionType + * @param amount + */ + private void checkoutToPlatformNotBill(Long userId, DeductionTypeEnum deductionType, Long amount) { + //生成订单编号 + BalanceCheckoutInput input = BalanceCheckoutInput.builder() + .platform("Balance") + .outTradeNo(KeyGenerator.instance().generatorUniqueKey("")) + .bizType(deductionType.getBizType()) + .name(deductionType.name()) + .srcAccountId(userId) + .desAccountId(-1L) + .productAmount(amount) + .platformFee(0L) + .promoAmount(0L) + .build(); + payClient.checkoutToPlatformNotBill(input); + } + + /** + * 保存用户流水 + * + * @param userId + * @param deductionType + * @param totalPreDeductionAmount + */ + private void saveUserBill(Long userId, DeductionTypeEnum deductionType, Long totalPreDeductionAmount) { + //生成订单编号 + BalanceCheckoutInput input = BalanceCheckoutInput.builder() + .platform("Balance") + .outTradeNo(KeyGenerator.instance().generatorUniqueKey("")) + .bizType(deductionType.getBizType()) + .name(deductionType.name()) + .srcAccountId(userId) + .desAccountId(-1L) + .productAmount(totalPreDeductionAmount) + .platformFee(0L) + .promoAmount(0L) + .build(); + payClient.saveUserBill(input); + } + + @Override + public Integer textOrVoiceTypeNoChatCheckoutJob() { + //扫描超过10分钟的没有文本和语音消息的聊天且预扣费金额大于0的记录 + List list = list(Wrappers.lambdaQuery() + .gt(UserDeductionStat::getPreDeductionAmount, 0) + .in(UserDeductionStat::getDeductionType, Lists.newArrayList(DeductionTypeEnum.TEXT.getIndex(), DeductionTypeEnum.VOICE.getIndex())) + .lt(UserDeductionStat::getLastTime, LocalDateTime.now().minusMinutes(10)) + ); + log.info("扫描超过10分钟的没有文本和语音消息的聊天且预扣费金额大于0的记录:{}", list); + if (CollectionUtils.isEmpty(list)) { + return 0; + } + for (UserDeductionStat userDeductionStat : list) { + //获取类型 + DeductionTypeEnum deductionTypeEnum = DeductionTypeEnum.getDeductionTypeEnum(userDeductionStat.getDeductionType()); + //预扣费金额大于0才发起扣款 + if (deductionTypeEnum != null && userDeductionStat.getPreDeductionAmount() > 0) { + try { + //保存用户流水 + saveUserBill(userDeductionStat.getUserId(), deductionTypeEnum, userDeductionStat.getPreDeductionAmount()); + //预扣费金额置为0 + updatePreDeductionAmountToZero(userDeductionStat.getId()); + } catch (Exception e) { + log.error("保存用户流水失败:", e); + } + } + } + return list.size(); + } + + /** + * 预扣费金额置为0 + * + * @param userDeductionStatId + */ + private void updatePreDeductionAmountToZero(Integer userDeductionStatId) { + update(Wrappers.lambdaUpdate() + .eq(UserDeductionStat::getId, userDeductionStatId) + .set(UserDeductionStat::getPreDeductionAmount, 0) + ); + } + + @Override + public Long getTotalDeductionAmount(Long userId) { + return userDeductionStatDao.getTotalDeductionAmount(userId); + } + + @Override + public void userDeductionCheckout(Long userId) { + //获取用户所有未完成扣费的记录 + List list = list(Wrappers.lambdaQuery() + .eq(UserDeductionStat::getUserId, userId) + .gt(UserDeductionStat::getPreDeductionAmount, 0) + ); + if (CollectionUtils.isEmpty(list)) { + return; + } + for (UserDeductionStat userDeductionStat : list) { + Long preDeductionAmount = userDeductionStat.getPreDeductionAmount(); + Integer deductionType = userDeductionStat.getDeductionType(); + //获取类型 + DeductionTypeEnum deductionTypeEnum = DeductionTypeEnum.getDeductionTypeEnum(deductionType); + //发起支付扣款 + saveUserBill(userId, deductionTypeEnum, preDeductionAmount); + //预扣费金额置为0 + updatePreDeductionAmountToZero(userDeductionStat.getId()); + } + } +} \ No newline at end of file diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserServiceImpl.java b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..f3e7645 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/service/impl/UserServiceImpl.java @@ -0,0 +1,132 @@ +package com.sonic.frog.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.bear.lib.client.UserNicknamePoolClient; +import com.sonic.bear.lib.client.UserSearchClient; +import com.sonic.bear.lib.client.UserSetClient; +import com.sonic.bear.lib.input.CompleteUserInfoInput; +import com.sonic.bear.lib.input.EditUserInfoInput; +import com.sonic.bear.lib.output.BaseUserInfoOutput; +import com.sonic.cow.lib.client.NsfwCheckClient; +import com.sonic.frog.domain.entity.AiUser; +import com.sonic.frog.domain.output.UserBaseInfoOutput; +import com.sonic.frog.enums.ToastResultCode; +import com.sonic.frog.service.AiUserSearchService; +import com.sonic.frog.service.AiUserService; +import com.sonic.frog.service.AiUserSetService; +import com.sonic.frog.service.UserService; +import com.sonic.frog.utils.BeanConver; +import com.sonic.frog.utils.MaskUtils; +import com.sonic.shark.lib.client.S3CheckClient; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import static com.sonic.frog.enums.Constants.*; + +@Slf4j +@Service +public class UserServiceImpl implements UserService { + + @Autowired + private UserSearchClient userSearchClient; + @Autowired + private UserSetClient userSetClient; + @Autowired + private UserNicknamePoolClient userNicknamePoolClient; + @Autowired + private S3CheckClient s3CheckClient; + @Autowired + private AiUserSearchService aiUserSearchService; + @Autowired + private NsfwCheckClient nsfwCheckClient; + @Autowired + private AiUserSetService aiUserSetService; + @Autowired + private AiUserService aiUserService; + + @Override + public BaseUserInfoOutput baseUserInfo(Long userId) { + return userSearchClient.baseUserInfo(userId); + } + + @Override + public UserBaseInfoOutput getbaseUserInfo(Long userId) { + BaseUserInfoOutput baseUserInfo = userSearchClient.baseUserInfo(userId); + UserBaseInfoOutput output = BeanConver.copeBean(baseUserInfo, UserBaseInfoOutput.class); + //已创建的AI数量 + Integer createdAiNum = aiUserSearchService.countAiNumByUserId(userId); + output.setCreatedAiCount(createdAiNum); + if (!CREATE_AI_NO_LIMIT_USER_ID_LIST.contains(userId)) { + //会员与普通用户可创建Ai数量 + Integer totalAiNum = output.getIsMember() != null && output.getIsMember() ? TOTAL_CREATE_AI_NUM : DEFAULT_CREATE_AI_NUM; + output.setCanCreateAiCount(totalAiNum); + } else { + output.setCanCreateAiCount(CREATE_AI_NO_LIMIT_NUM); + } + + //处理邮箱脱敏 + output.setThirdEmail(MaskUtils.maskEmail(baseUserInfo.getThirdEmail())); + return output; + } + + @Override + public void completeUserInfo(CompleteUserInfoInput input) { + //年龄校验,不能小于18岁 + ToastResultCode.AGE_CHECK.check(input.getBirthday().isAfter(LocalDateTime.now().minusYears(18))); + ToastResultCode.PARAM_NOT_NULL.check(StringUtils.isEmpty(input.getNickname()) || StringUtils.isEmpty(input.getNickname().trim())); + ToastResultCode.PARAM_LEN_MIN_ERROR.check(input.getNickname().length() < 2); + //昵称敏感词校验,昵称不允许包含敏感词 + nsfwCheckClient.checkContent(input.getNickname()); + + //昵称校验,昵称不允许重复 + boolean nicknameExist = userNicknamePoolClient.userNicknameExistCheck(input.getUserId(), input.getNickname()); + ToastResultCode.USER_NICKNAME_EXIST.check(nicknameExist); + + //头像地址设置为空 + input.setHeadImage(null); + userSetClient.completeUserInfo(input); + } + + @Override + public void editUserInfo(EditUserInfoInput input) { + //年龄校验,不能小于18岁 + ToastResultCode.AGE_CHECK.check(input.getBirthday() != null && input.getBirthday().isAfter(LocalDateTime.now().minusYears(18))); + if (StringUtils.isNotEmpty(input.getNickname())) { + //昵称敏感词校验,昵称不允许包含敏感词 + nsfwCheckClient.checkContent(input.getNickname()); + //昵称校验,昵称不允许重复 + boolean nicknameExist = userNicknamePoolClient.userNicknameExistCheck(input.getUserId(), input.getNickname()); + ToastResultCode.USER_NICKNAME_EXIST.check(nicknameExist); + } + if (StringUtils.isNotEmpty(input.getHeadImage())) { + //头像校验,头像地址是否是符合规范,鉴黄是否通过 + s3CheckClient.checkImage(input.getHeadImage()); + } + //执行更新操作 + userSetClient.editUserInfo(input); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void delAccount(Long userId) { + //查询当前用户的所有 AI 列表 + List aiIdList = aiUserService.list(Wrappers.lambdaQuery() + .select(AiUser::getAiId) + .eq(AiUser::getUserId, userId).eq(AiUser::getIsDelete, false)) + .stream().map(e -> e.getAiId()).collect(Collectors.toList()); + for (Long aiId : aiIdList) { + //删除AI数据 + aiUserSetService.delAiUser(aiId, userId); + } + //删除账号 + userSetClient.delAccount(userId); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/AbstractKeyGenerator.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/AbstractKeyGenerator.java new file mode 100644 index 0000000..74a636f --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/AbstractKeyGenerator.java @@ -0,0 +1,76 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.sonic.frog.utils; + + +import com.sonic.frog.enums.ToastResultCode; + +import java.net.InetAddress; +import java.text.NumberFormat; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class AbstractKeyGenerator { + private static final int SEQ_MIN = 1; + private static final int SEQ_MAX = 999; + private static final int SEQ_DIGITS = 3; + private static final AtomicInteger SEQ_ATOMIC_INTEGER = new AtomicInteger(1); + private static final char IP_SPACER = '.'; + private static final String IP_AFTER_TWO; + private static final int IP_DIGITS = 2; + private static final int IP_MOD = 99; + private static final String PATTERN = "yyyyMMddHHmmssSSS"; + private static final int ORDER_NO_MAX_SIZE = 29; + + public AbstractKeyGenerator() { + } + + public String generatorUniqueKey(String businessCode) { + StringBuffer buffer = new StringBuffer(); + String date = DateUtils.getNow(PATTERN); + buffer.append(this.customKey()).append(businessCode).append(date).append(IP_AFTER_TWO).append(formatNumber((long)getSeq(), SEQ_DIGITS)); + + ToastResultCode.SYS_SYSTEM_EXCEPTION.check(buffer.length() > ORDER_NO_MAX_SIZE, "tradeNo to long, tradeNo->" + buffer.toString()); + return buffer.toString(); + } + + public abstract String customKey(); + + private static int getSeq() { + int result = SEQ_ATOMIC_INTEGER.incrementAndGet(); + if (result <= SEQ_MAX) { + return result; + } else { + SEQ_ATOMIC_INTEGER.set(SEQ_MIN); + return SEQ_MIN; + } + } + + private static String formatNumber(long number, int digits) { + NumberFormat nf = NumberFormat.getInstance(); + nf.setMaximumIntegerDigits(digits); + nf.setMinimumIntegerDigits(digits); + nf.setGroupingUsed(false); + return nf.format(number); + } + + static { + String ip; + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + ip = inetAddress.getHostAddress(); + ip = ip.substring(ip.lastIndexOf(46) + 1); + ip = formatNumber(Long.valueOf(ip), IP_DIGITS); + } catch (Exception var4) { + Random random = new Random((long)UUID.randomUUID().toString().hashCode()); + int randomNum = random.nextInt(IP_MOD); + ip = formatNumber((long)randomNum, IP_DIGITS); + } + + IP_AFTER_TWO = ip; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/AppleThirdUtils.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/AppleThirdUtils.java new file mode 100644 index 0000000..ebdfdc9 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/AppleThirdUtils.java @@ -0,0 +1,246 @@ +package com.sonic.frog.utils; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import io.jsonwebtoken.*; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.HashMap; +import java.util.Map; + +/** + * 苹果登录工具类 + */ +public class AppleThirdUtils { + + @Autowired + static RestTemplate restTemplate; + + private static final Logger logger = LoggerFactory.getLogger(AppleThirdUtils.class); + + /** + * client_id (应用id,从苹果注册应用获取) + */ + public static final String APPLICATION_ID = "gg.sonic"; + + /** + * 密钥key(从txt文件中获取) + */ + private static final String SECRET_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgRbQpSurTdkoWEfJwaMYRXTdXh/Q9oO3UMsvAv8cUGOagCgYIKoZIzj0DAQehRANCAAQZ1o16rbIdDLhPiINxMwz4HyGh5VVRp+RIf4I8GHRBmhGbBiu4AomiOvANzu3rQt2i0z136q4OUucbqG7SCh43"; + + /** + * p8文件中获取的kid + */ + private static final String FILE_KID = "WTP5D3C65M"; + + /** + * p8文件中获取的team_id + */ + private static final String TEAM_ID = "S68QCFB8Q7"; + + /** + * 固定值(用于验证token接口) + */ + private static final String GRANT_TYPE = "authorization_code"; + + /** + * 获取公钥地址 + */ + private static final String PUBLIC_KEY_URL = "https://appleid.apple.com/auth/keys"; + + /** + * 获取验证token地址 + */ + private static final String GET_ID_TOKEN = "https://appleid.apple.com/auth/token"; + + /** + * 苹果官网地址 + */ + private static final String APPLE_URL = "https://appleid.apple.com"; + + /** + * 苹果验证成功后返回的用户信息中的登录时间 + */ + private static final String AUTH_TIME = "auth_time"; + + + /** + * 获取验证的code + */ + private static String getValidateCode(String code) { + restTemplate = new RestTemplate(); + //请求苹果验证接口 + ResponseEntity response = restTemplate.postForEntity(GET_ID_TOKEN, AppleThirdUtils.getRequestParams(code), String.class); + + return response.getBody(); + } + + /** + * 构建验证登录参数 + * + * @author WangDeyu + */ + private static HttpEntity> getRequestParams(String code) { + + //构建请求参数 + HttpHeaders headers = new HttpHeaders(); + MultiValueMap map = new LinkedMultiValueMap<>(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + map.add("client_id", APPLICATION_ID); + map.add("client_secret", getSecretKey()); + map.add("code", code); + map.add("grant_type", GRANT_TYPE); + + return new HttpEntity<>(map, headers); + } + + /** + * 读取文件中的密钥key,解密 + */ + private static byte[] readKey() { + return Base64.decodeBase64(SECRET_KEY); + } + + /** + * 获取秘钥 + */ + private static String getSecretKey() { + try { + Map header = new HashMap<>(16); + // 参考后台配置kid + header.put("kid", FILE_KID); + header.put("typ","JWT"); + Map claims = new HashMap<>(16); + // 参考后台配置 team id + claims.put("iss", TEAM_ID); + long now = System.currentTimeMillis() / 1000; + claims.put("iat", now); + // 最长半年,单位秒 + claims.put("exp", now + 86400 * 30); + // 苹果官网网址 + claims.put("aud", APPLE_URL); + // client_id (应用id) + claims.put("sub", APPLICATION_ID); + PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(readKey()); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec); + + return Jwts.builder().setHeader(header).setClaims(claims).signWith( privateKey,SignatureAlgorithm.ES256).compact(); + } catch (Exception e) { + logger.error("获取apple密钥失败:", e); + throw new RuntimeException("获取apple密钥失败:", e); + } + } + + + /** + * 解密个人信息 + * + * @param identityToken APP获取的identityToken + * @return 解密参数:失败返回null + */ + public static String verify(String identityToken) { + try { + if (identityToken.split("\\.").length <= 1) { + return null; + } + String firstDate = new String(Base64.decodeBase64(identityToken.split("\\.")[0]), "UTF-8"); + String claim = new String(Base64.decodeBase64(identityToken.split("\\.")[1])); + String kid = JSONObject.parseObject(firstDate).get("kid").toString(); + String aud = JSONObject.parseObject(claim).get("aud").toString(); + String sub = JSONObject.parseObject(claim).get("sub").toString(); + String response = verify(getPublicKey(kid), identityToken, aud, sub); + + return "SUCCESS".equals(response) ? claim : null; + } catch (Exception e) { + logger.error("解密TOKEN失败", e); + throw new RuntimeException("解密TOKEN失败", e); + } + } + + /** + * 验证token + */ + private static String verify(PublicKey key, String jwt, String audience, String subject) throws Exception { + String result = "FAIL"; + JwtParser jwtParser = Jwts.parser().setSigningKey(key); + jwtParser.requireIssuer(APPLE_URL); + jwtParser.requireAudience(audience); + jwtParser.requireSubject(subject); + try { + Jws claim = jwtParser.parseClaimsJws(jwt); + if (claim != null && claim.getBody().containsKey(AUTH_TIME)) { + result = "SUCCESS"; + return result; + } + } catch (ExpiredJwtException e) { + logger.error("苹果token过期", e); + throw new Exception("苹果token过期", e); + } catch (SignatureException e) { + logger.error("苹果token非法", e); + throw new Exception("苹果token非法", e); + } + return result; + } + + + /** + * 获取苹果公钥 + * + * @param kid (公钥的id) + * @return PublicKey + */ + private static PublicKey getPublicKey(String kid) { + try { + restTemplate = new RestTemplate(); + //请求苹果验证接口 + String response = restTemplate.getForObject(PUBLIC_KEY_URL, String.class); + if (StringUtils.isBlank(response)) { + return null; + } + JSONObject data = JSONObject.parseObject(response); + JSONArray jsonArray = data.getJSONArray("keys"); + if (jsonArray.isEmpty()) { + return null; + } + //通过kid,n和e的值获取苹果公钥字符串 + for (Object object : jsonArray) { + JSONObject json = ((JSONObject) object); + //公钥有多个,只取第一个会报SignatureException异常,得根据token的kid取 + if (json.getString("kid").equals(kid)) { + String n = json.getString("n"); + String e = json.getString("e"); + BigInteger modulus = new BigInteger(1, Base64.decodeBase64(n)); + BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(e)); + RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(spec); + } + } + } catch (Exception e) { + logger.error("getPublicKey异常! {}", e.getMessage()); + e.printStackTrace(); + } + return null; + + } + +} + diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/BeanConver.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/BeanConver.java new file mode 100644 index 0000000..3386cb8 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/BeanConver.java @@ -0,0 +1,66 @@ +package com.sonic.frog.utils; + +import com.sonic.common.rpc.Page; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cglib.beans.BeanCopier; + +import java.util.ArrayList; +import java.util.List; + +/** + * 对象拷贝 + * @author dev-center + */ +public class BeanConver { + private final static Logger LOG = LoggerFactory.getLogger(BeanConver.class); + + /** + * 实例类转换 + * + * @param source 源对象 + * @param target 目标对象 + * @param 源对象 + * @param 目标对象 + */ + public static K copeBean(T source, Class target) { + if (source == null) { + return null; + } + BeanCopier beanCopier = BeanCopier.create(source.getClass(), target, false); + K result = null; + try { + result = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(source, result, null); + return result; + } + + + /** + * page实例类转换 + * + * @param 源对象 + * @param 目标对象 + * @param source 源对象 + * @param target 目标对象 + * @return + */ + public static List copeList(List source, Class target) { + List list = new ArrayList<>(); + for (T af : source) { + BeanCopier beanCopier = BeanCopier.create(af.getClass(), target, false); + K af1 = null; + try { + af1 = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(af, af1, null); + list.add(af1); + } + return list; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/BeanConvert.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/BeanConvert.java new file mode 100644 index 0000000..89ee98c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/BeanConvert.java @@ -0,0 +1,64 @@ +package com.sonic.frog.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cglib.beans.BeanCopier; + +import java.util.ArrayList; +import java.util.List; + +/** + * 对象拷贝 + * @author dev-center + */ +public class BeanConvert { + private final static Logger LOG = LoggerFactory.getLogger(BeanConvert.class); + + /** + * 实例类转换 + * + * @param source 源对象 + * @param target 目标对象 + * @param 源对象 + * @param 目标对象 + */ + public static K copeBean(T source, Class target) { + if (source == null) { + return null; + } + BeanCopier beanCopier = BeanCopier.create(source.getClass(), target, false); + K result = null; + try { + result = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(source, result, null); + return result; + } + + /** + * page实例类转换 + * + * @param 源对象 + * @param 目标对象 + * @param source 源对象 + * @param target 目标对象 + * @return + */ + public static List copeList(List source, Class target) { + List list = new ArrayList<>(); + for (T af : source) { + BeanCopier beanCopier = BeanCopier.create(af.getClass(), target, false); + K af1 = null; + try { + af1 = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(af, af1, null); + list.add(af1); + } + return list; + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/CacheUtils.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/CacheUtils.java new file mode 100644 index 0000000..c3d1867 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/CacheUtils.java @@ -0,0 +1,155 @@ +package com.sonic.frog.utils; + +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 统一的缓存工具类 + * @Author zzhan + * @Date 2021/3/4 11:27 + * @Version 1.0 + */ +@Component +public class CacheUtils { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * 清理String类型的缓存数据 + * @param redisKey + */ + public void clearStringCache(String redisKey) { + stringRedisTemplate.delete(redisKey); + } + + /** + * 获取指定类型的缓存对象数据 + * @param redisKey + * @param clazz + * @param + * @return + */ + public T getCacheObject(String redisKey, Class clazz) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKey); + if(StringUtils.isEmpty(cacheStr)) { + return null; + } + return JSONObject.parseObject(cacheStr, clazz); + } + + /** + * 获取指定类型的缓存列表对象数据 + * @param redisKey + * @param clazz + * @param + * @return + */ + public List getCacheList(String redisKey, Class clazz) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKey); + if(StringUtils.isEmpty(cacheStr)) { + return null; + } + return JSONObject.parseArray(cacheStr, clazz); + } + + /** + * 获取缓存对象数据,如果缓存数据不存在的话则设置缓存对象 + * @param redisKey + * @param clazz + * @param abstractCache + * @param timeSeconds + * @param + * @return + */ + public T getCacheObjectAndSet(String redisKey, Class clazz, AbstractCache abstractCache, int timeSeconds) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKey); + if(StringUtils.isNotEmpty(cacheStr)) { + return JSONObject.parseObject(cacheStr, clazz); + } + String param = abstractCache.getData(); + if(param == null) { + return null; + } + setCache(redisKey, param, timeSeconds); + return JSONObject.parseObject(param, clazz); + } + + /** + * 获取缓存对象数据,如果缓存数据不存在的话则设置缓存对象 + * @param redisKey + * @param abstractCache + * @param timeSeconds + * @return + */ + public String getCacheStringAndSet(String redisKey, AbstractCache abstractCache, int timeSeconds) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKey); + if(StringUtils.isNotEmpty(cacheStr)) { + return cacheStr; + } + String param = abstractCache.getData(); + if(param == null) { + return null; + } + setCache(redisKey, param, timeSeconds); + return param; + } + + /** + * 获取缓存列表数据,如果缓存数据不存在的话则设置缓存列表 + * @param redisKey + * @param clazz + * @param abstractCache + * @param timeSeconds + * @param + * @return + */ + public List getCacheListAndSet(String redisKey, Class clazz, AbstractCache abstractCache, int timeSeconds) { + String cacheStr = stringRedisTemplate.opsForValue().get(redisKey); + if(StringUtils.isNotEmpty(cacheStr)) { + return JSONObject.parseArray(cacheStr, clazz); + } + String param = abstractCache.getData(); + if(param == null) { + return null; + } + setCache(redisKey, param, timeSeconds); + return JSONObject.parseArray(param, clazz); + } + + /** + * 设置缓存数据 + * @param redisKey + * @param param + * @param timeSeconds + */ + public void setCache(String redisKey, String param, int timeSeconds) { + stringRedisTemplate.opsForValue().set(redisKey, param, timeSeconds, TimeUnit.SECONDS); + } + + /** + * 清理缓存数据 + * @param redisKey + */ + public void removeCache(String redisKey) { + stringRedisTemplate.delete(redisKey); + } + + + public interface AbstractCache { + + /** + * 获取需要存储到缓存的数据 + * @return + */ + String getData(); + + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/CheckUtils.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/CheckUtils.java new file mode 100644 index 0000000..58d849b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/CheckUtils.java @@ -0,0 +1,85 @@ +package com.sonic.frog.utils; + + + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * @Author zzhan + * @Description TODO + * @Date 2024/3/7 13:37 + * @Version 1.0 + */ +public class CheckUtils { + + public static String[] BASE_CHARS = new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", + "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", + "T", "U", "V", "W", "X", "Y", "Z"}; + + + + /** + * 生成有时间戳编码的30位的UUID + * @return + */ + public static String genUUIDAnEncode() { + //获取当前时间戳 + Long time = System.currentTimeMillis() / 1000; + //将时间戳按照每2个字符进行拆分 + List timeList = splitTimestamp(time); + //拼接出字符串的UUID,30位 + StringBuffer uuid = new StringBuffer(); + uuid.append(randomUUID(2)).append(timeList.get(0)); + uuid.append(randomUUID(2)).append(timeList.get(1)); + uuid.append(randomUUID(2)).append(timeList.get(2)); + uuid.append(randomUUID(2)).append(timeList.get(3)); + uuid.append(randomUUID(2)).append(timeList.get(4)); + return uuid.toString(); + } + + /** + * 反解出时间戳 + * @param uuid + * @return + */ + public static Long uuidDecodeToTime(String uuid) { + //从指定位置截取数字部分并转换为长整型 + String digits = uuid.substring(2, 4) + uuid.substring(6, 8) + uuid.substring(10, 12) + + uuid.substring(14, 16) + uuid.substring(18, 20); + return Long.valueOf(digits); + } + + /** + * 生成指定长度的随机字符串 + * @param size + * @return + */ + public static String randomUUID(int size) { + Random random = new Random(); + StringBuffer randomStr = new StringBuffer(); + //生成10位的随机字符 + for (int i = 0; i < size; i++) { + int index = random.nextInt(52); + randomStr.append(BASE_CHARS[index]); + } + return randomStr.toString(); + } + + /** + * 将时间戳按照每2位进行分段 + * @param timestamp + * @return + */ + public static List splitTimestamp(long timestamp) { + String timestampStr = String.valueOf(timestamp); + List timestampList = new ArrayList<>(); + for (int i = 0; i < timestampStr.length(); i += 2) { + timestampList.add(timestampStr.substring(i, Math.min(i + 2, timestampStr.length()))); + } + return timestampList; + } + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/DateConvertUtils.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/DateConvertUtils.java new file mode 100644 index 0000000..3208d83 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/DateConvertUtils.java @@ -0,0 +1,429 @@ +package com.sonic.frog.utils; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; + +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.time.temporal.WeekFields; +import java.time.zone.ZoneRules; +import java.util.*; + +/** + * code + */ +@Slf4j +public class DateConvertUtils { + + + /** + * 获取本周周一的日期 + * @return + */ + public static String getMondayOfThisWeek() { + LocalDate monday = LocalDate.now().with(TemporalAdjusters.previous(DayOfWeek.SUNDAY)).plusDays(1); + return monday.toString(); + } + + /** + * 根据年和周数获取周的所有日期时间 + * @param year + * @param num + * @param dayOfWeekList + * @return + */ + public static List getDateByYearAndWeekNumAndDayOfWeek(Integer year, Integer num, List dayOfWeekList) { + List result = Lists.newArrayList(); + if(CollectionUtils.isEmpty(dayOfWeekList)) { + dayOfWeekList = Lists.newArrayList(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY, DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); + } + //周数小于10在前面补个0 + String numStr = num < 10 ? "0" + String.valueOf(num) : String.valueOf(num); + //2019-W01-01获取第一周的周一日期,2019-W02-07获取第二周的周日日期 + for (DayOfWeek dayOfWeek : dayOfWeekList) { + String weekDate = String.format("%s-W%s-%s", year, numStr, dayOfWeek.getValue()); + LocalDate localDate = LocalDate.parse(weekDate, DateTimeFormatter.ISO_WEEK_DATE); + result.add(localDate); + } + return result; + } + + /** + * 获取当前年中的周 + * @return + */ + public static int getYearWeek() { + LocalDateTime now = ZonedDateTime.now(ZoneId.of("America/Los_Angeles")).toLocalDateTime(); + WeekFields weekFields = WeekFields.of(DayOfWeek.MONDAY,4); + int week = now.get(weekFields.weekOfWeekBasedYear()); + return week; + } + + public static String format(LocalDateTime localDateTime, String pattern) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); + //美国洛杉矶时间 + String date = dateTimeFormatter.format(localDateTime.minusHours(15)); + return date; + } + + public static String format(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + //美国洛杉矶时间 + String date = dateTimeFormatter.format(localDateTime.minusHours(15)); + return date; + } + + /** + * 将符合 "yyyy-MM-dd" 格式的日期字符串转换为 LocalDateTime 对象。 + * + * @param dateString 日期字符串 + * @return LocalDateTime 对象 + */ + public static LocalDateTime convertStringToDateTime(String dateString) { + // 定义DateTimeFormatter,指定日期格式 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + // 将字符串解析为LocalDate + LocalDate date = LocalDate.parse(dateString, formatter); + // 将LocalDate转换为LocalDateTime,时间部分设为00:00 + LocalDateTime dateTime = date.atStartOfDay(); + return dateTime; + } + + /** + * 将符合 "yyyy-MM-dd" 格式的日期字符串转换为 LocalDateTime 对象。 + * + * @param dateString 日期字符串 + * @return LocalDateTime 对象 + */ + public static LocalDateTime convertStringToDateTimeV2(String dateString) { + // 定义DateTimeFormatter,指定日期格式 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + // 将字符串解析为LocalDate + LocalDate date = LocalDate.parse(dateString, formatter); + // 将LocalDate转换为LocalDateTime,时间部分设为00:00 + LocalDateTime dateTime = date.atStartOfDay(); + return dateTime; + } + + /** + * 计算两个 LocalDateTime 对象之间的天数差。 + * + * @param startDate 起始日期时间 + * @param endDate 结束日期时间 + * @return 两个日期相差的天数,如果 startDate 在 endDate 之后,返回负数 + */ + public static long calculateDaysBetween(LocalDateTime startDate, LocalDateTime endDate) { + return ChronoUnit.DAYS.between(startDate, endDate); + } + + /** + * 计算两个时间相差的秒数 + * @param startDate + * @param endDate + * @return + */ + public static long calculateDaysBetweenSeconds(LocalDateTime startDate, LocalDateTime endDate) { + return ChronoUnit.SECONDS.between(startDate, endDate); + } + + public static String noTimeZoneChangeFormat(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = dateTimeFormatter.format(localDateTime); + return date; + } + + public static String noTimeZoneChangeFormat(LocalDate localDate) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = localDate.format(dateTimeFormatter); + return date; + } + + public static String format() { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + //美国洛杉矶时间 + String date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("America/Los_Angeles")).toLocalDateTime()); + return date; + } + + public static String format(Date date) { + try { + SimpleDateFormat dateTimeFormatter = new SimpleDateFormat("yyyy-MM-dd"); + String result = dateTimeFormatter.format(date); + return result; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static String formatHms(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + //美国洛杉矶时间 + String date = dateTimeFormatter.format(localDateTime); + return date; + } + + public static String formatYearMonthDay(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + //美国洛杉矶时间 + String date = dateTimeFormatter.format(localDateTime); + return date; + } + + public static String formatYearMonth(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM"); + //美国洛杉矶时间 + String date = dateTimeFormatter.format(localDateTime); + return date; + } + + public static Integer formatYearMonthInt(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM"); + //美国洛杉矶时间 + String date = dateTimeFormatter.format(localDateTime); + return Integer.valueOf(date); + } + + public static LocalDateTime getDateMinTime(){ + LocalDateTime now = ZonedDateTime.now(ZoneId.of("America/Los_Angeles")).toLocalDateTime() + .with(LocalTime.MIN); + return now; + } + + public static LocalDateTime getDateMinTime(LocalDateTime localDateTime){ + LocalDateTime now = localDateTime.now(ZoneId.of("America/Los_Angeles")) + .with(LocalTime.MIN); + return now; + } + + public static LocalDateTime getDateMaxTime(){ + LocalDateTime now = ZonedDateTime.now(ZoneId.of("America/Los_Angeles")).toLocalDateTime() + .with(LocalTime.MAX); + return now; + } + + public static LocalDateTime getDateMaxTime(LocalDateTime localDateTime){ + LocalDateTime now = localDateTime.now(ZoneId.of("America/Los_Angeles")) + .with(LocalTime.MAX); + return now; + } + + public static LocalDateTime getDateLaTime(){ + LocalDateTime now = ZonedDateTime.now(ZoneId.of("America/Los_Angeles")).toLocalDateTime(); + return now; + } + + + /** + * 这里有个坑,传入的时间不是指参数转成La时间而是.now.... + * @param localDateTime + * @return + */ + @Deprecated + public static LocalDateTime getDateLaTime(LocalDateTime localDateTime){ + LocalDateTime now = localDateTime.now(ZoneId.of("America/Los_Angeles")); + return now; + } + + public static Integer formatToLocalInt(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + String date = dateTimeFormatter.format(localDateTime); + return Integer.valueOf(date); + } + + /** + * 获取洛杉矶 夏令时、冬令时 和北京时间的小时差 + * @return + */ + public static int getLaHours() { + Duration duration = Duration.between(getDateLaTime(), LocalDateTime.now()); + Long hours = duration.toHours();//相差的小时数 + return hours.intValue(); + } + + /** + * 传入指定时间和时区 + * @param localDateTime + * @param zoneId + * @return true 表示是夏令时,false表示是冬令时 + */ + public static boolean isDaylightTime(LocalDateTime localDateTime, ZoneId zoneId) { + ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId); + ZoneRules rules = zoneId.getRules(); + boolean flag = rules.isDaylightSavings(zonedDateTime.toInstant()); + return flag; + } + + /** + * 获取两个时间相差的秒数 + * @param startTime + * @param endTime + * @return + */ + public static Long getTimeDifference(LocalDateTime startTime, LocalDateTime endTime) { + Duration duration = Duration.between(startTime, endTime); + return duration.getSeconds(); + } + + /** + * 获得某个月最大天数 + * @param year 年份 + * @param month 月份 (1-12) + * @return 某个月最大天数 + */ + public static int getMaxDayByYearMonth(int year, int month) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.DATE, 1); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month - 1); + return calendar.getActualMaximum(Calendar.DATE); + } + + + /** + * 获取最大连续天数 + * @param dayList + * @return + */ + public static Integer maxConsecutiveDays(TreeSet dayList) { + try { + List list = new ArrayList<>(); + for (Integer day : dayList) { + list.add(Integer.valueOf(day.toString().substring(6, 8))); + } + int year = LocalDateTime.now().minusHours(15).getYear(); + int month = LocalDateTime.now().minusHours(15).getMonthValue() - 1; + if(month < 0) { + year = year - 1; + month = 12; + } + int maxDay = DateConvertUtils.getMaxDayByYearMonth(year, month); + System.out.println(maxDay); + int day = 0; + int index = 0; + for (int i = 0; i < list.size(); i++) { + if(day > list.get(i)) { + index = i; + break; + } + day = list.get(i); + } + List lastMonthList = list.subList(0, index); + List monthList = list.subList(index, list.size()); + TreeSet treeSet = new TreeSet(monthList); + if(CollectionUtils.isNotEmpty(lastMonthList)) { + for (Integer lastMonthDay : lastMonthList) { + treeSet.add(lastMonthDay - maxDay); + } + } + Map groupDateCountMap = new HashMap<>(); + int i = 0; + for (Integer value : treeSet) { + value = value - i; + Integer count = groupDateCountMap.get(value); + count = count == null ? 1 : count + 1; + groupDateCountMap.put(value, count); + i++; + } + if(groupDateCountMap.keySet().size() > 0) { + //获取最大的连续登录天数 + Integer cond = groupDateCountMap.values().stream().max(Comparator.comparing(e -> e)).get(); + return cond == null ? 0 : cond; + } + return dayList.size(); + } catch (NumberFormatException e) { + return dayList.size(); + } + } + + /** + * 获取指定年、周数的日期 (参考:https://blog.csdn.net/weixin_44919928/article/details/100008249) + * @param year + * @param num + * @param dayOfWeek(MONDAY TUESDAY) + * @return + */ + public static LocalDate getDateByYearAndWeekNumAndDayOfWeekParam(Integer year, Integer num, DayOfWeek dayOfWeek) { + //周数小于10在前面补个0 + String numStr = num < 10 ? "0" + String.valueOf(num) : String.valueOf(num); + //2019-W01-01获取第一周的周一日期,2019-W02-07获取第二周的周日日期 + String weekDate = String.format("%s-W%s-%s", year, numStr, dayOfWeek.getValue()); + return LocalDate.parse(weekDate, DateTimeFormatter.ISO_WEEK_DATE); + } + + /** + * 判断开始的年月是否大于结束的年月 + * @param startYearMonth + * @param endYearMonth + * @return + */ + public static Boolean yearMonthCheck(String startYearMonth, String endYearMonth) { + int startYearMonthInt = Integer.valueOf(startYearMonth.replace("-", "")); + int endYearMonthInt = Integer.valueOf(endYearMonth.replace("-", "")); + return startYearMonthInt > endYearMonthInt; + } + + + /** + * 检查今天是否在起止时间段内 + * @param currentDay + * @param startDay + * @param endDay + * @return + */ + public static Boolean currentDayInRangeDayCheck(String currentDay, String startDay, String endDay) { + int startDayInt = Integer.valueOf(startDay.replace("-", "")); + int endDayInt = Integer.valueOf(endDay.replace("-", "")); + int currentDayInt = Integer.valueOf(currentDay.replace("-", "")); + return currentDayInt >= startDayInt && currentDayInt <= endDayInt; + } + + /** + * 获取指定时间段内的日志列表 + * @param start + * @param end + * @return + */ + public static List getDateStringsBetween(String start, String end) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate startDate = LocalDate.parse(start, formatter); + LocalDate endDate = LocalDate.parse(end, formatter); + + List dateStrings = new ArrayList<>(); + LocalDate temp = startDate; + + while (!temp.isAfter(endDate)) { + dateStrings.add(temp.format(formatter)); + temp = temp.plus(1, ChronoUnit.DAYS); + } + + return dateStrings; + } + + public static LocalDateTime convertTimeZonePstToCtt(LocalDateTime dateTime) { + ZoneId fromZone = ZoneId.of("America/Los_Angeles"); + ZoneId toZone = ZoneId.of("Asia/Shanghai"); + // 将LocalDateTime转换为ZonedDateTime,使用源时区 + ZonedDateTime fromZonedDateTime = ZonedDateTime.of(dateTime, fromZone); + + // 将ZonedDateTime转换为目标时区的ZonedDateTime + ZonedDateTime toZonedDateTime = fromZonedDateTime.withZoneSameInstant(toZone); + + // 返回目标时区的LocalDateTime + return toZonedDateTime.toLocalDateTime(); + } + + /** + * @param args + */ + public static void main(String[] args) { + + System.out.println("第" + 1 + "周,周一日期:" + getDateByYearAndWeekNumAndDayOfWeekParam(2021, 1, DayOfWeek.MONDAY).toString()); + System.out.println("第" + 1 + "周,周一日期:" + getDateByYearAndWeekNumAndDayOfWeekParam(2021, 1, DayOfWeek.SUNDAY).toString()); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/DateUtil.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/DateUtil.java new file mode 100644 index 0000000..c40eed0 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/DateUtil.java @@ -0,0 +1,845 @@ +package com.sonic.frog.utils; + + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateFormatUtils; +import org.joda.time.DateTime; +import org.joda.time.Days; + +import java.sql.Time; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.time.chrono.IsoChronology; +import java.util.*; + +import static org.reflections.Reflections.log; + +/** + * 严格的日期转换setLenient(false); setLenient public void setLenient(boolean + * lenient)指定日期/时间解析是否不严格。进行不严格解析时,解析程序可以使用启发式的方法来解释与此对象的格式不精确匹配的输入。进行严格解析时, 输入必须匹配此对象的格式。 参数: + * lenient - 为 true 时,解析过程是不严格的 不会自动将错误日期转换为正确的日期 例如:19450000,使用原DateUtil会转换为19441130 + */ +public class DateUtil { + public static final String COMPACT_DATE_FORMAT = "yyyyMMdd"; + public static final String YM = "yyyyMM"; + public static final String YMD = "yyyyMMdd"; + public static final String YMDHMS = "yyyyMMddHHmmss"; + public static final String NORMAL_DATE_FORMAT = "yyyy-MM-dd"; + public static final String NORMAL_DATE_FORMAT_NEW = "yyyy-mm-dd hh24:mi:ss"; + public static final String DATE_FORMAT = "yyyy-MM-dd"; + public static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String DATE_ALL = "yyyyMMddHHmmssS"; + public static final String DETE_ALL_SIX = "yyMMddHHmmssSSSSSS"; + public static final String DATE_FORMAT_SLASH = "yyyy/MM/dd"; + public static final String DATETIME_FORMAT_SLASH = "yyyy/MM/dd HH:mm:ss"; + public static final String DATETIME_FORMAT_T = "yyyy-MM-ddTHHmmss"; + public static final String DATETIME_FORMAT_START = "yyyy-MM-dd 00:00:00"; + public static final String DATETIME_FORMAT_END = "yyyy-MM-dd 23:59:59"; + public static final String DATETIME_FORMAT_ZH = "yyyy年MM月dd日"; + + public static final ZoneId ZONE_ID = ZoneId.systemDefault(); + + private static final SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + public static Long strDateToNum(String paramString) throws Exception { + if (paramString == null) { + return null; + } + String[] arrayOfString = null; + String str = ""; + if (paramString.indexOf("-") >= 0) { + arrayOfString = paramString.split("-"); + for (int i = 0; i < arrayOfString.length; ++i) { + str = str + arrayOfString[i]; + } + return Long.valueOf(Long.parseLong(str)); + } + return Long.valueOf(Long.parseLong(paramString)); + } + + public static Long strDateToNum1(String paramString) throws Exception { + if (paramString == null) { + return null; + } + String[] arrayOfString = null; + String str = ""; + if (paramString.contains("-")) { + arrayOfString = paramString.split("-"); + for (String s : arrayOfString) { + if (s.length() == 1) { + str = str + "0" + s; + } else { + str = str + s; + } + } + return Long.parseLong(str); + } + return Long.parseLong(paramString); + } + + public static String numDateToStr(Long paramLong) { + if (paramLong == null) { + return null; + } + String str = null; + str = paramLong.toString().substring(0, 4) + "-" + paramLong.toString().substring(4, 6) + "-" + + paramLong.toString().substring(6, 8); + return str; + } + + public static Date stringToDate(String paramString1, String paramString2) throws Exception { + SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat(paramString2); + localSimpleDateFormat.setLenient(false); + try { + return localSimpleDateFormat.parse(paramString1); + } catch (ParseException localParseException) { + throw new Exception("解析日期字符串时出错!"); + } + } + + public static String dateToString(Date paramDate, String paramString) { + SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat(paramString); + localSimpleDateFormat.setLenient(false); + return localSimpleDateFormat.format(paramDate); + } + + public static Date compactStringToDate(String paramString) throws Exception { + return stringToDate(paramString, "yyyyMMdd"); + } + + public static String dateToCompactString(Date paramDate) { + return dateToString(paramDate, "yyyyMMdd"); + } + + public static String dateToNormalString(Date paramDate) { + return dateToString(paramDate, "yyyy-MM-dd"); + } + + public static String dateToNormalMDString(Date paramDate) { + return dateToString(paramDate, "MM/dd"); + } + + public static String dateToTString(Date paramDate) { + return DateFormatUtils.format(paramDate, "yyyy-MM-dd") + "T" + DateFormatUtils.format(paramDate, "HHmmss"); + } + + public static String dateToMSecondString(Date paramDate) { + return dateToString(paramDate, "yyyyMMddHHmmssSSS"); + } + + public static String compactStringDateToNormal(String paramString) throws Exception { + return dateToNormalString(compactStringToDate(paramString)); + } + + public static int getDaysBetween(Date paramDate1, Date paramDate2) throws Exception { + Calendar localCalendar1 = Calendar.getInstance(); + Calendar localCalendar2 = Calendar.getInstance(); + localCalendar1.setTime(paramDate1); + localCalendar2.setTime(paramDate2); + if (localCalendar1.after(localCalendar2)) { + throw new Exception("起始日期小于终止日期!"); + } + int i = localCalendar2.get(6) - localCalendar1.get(6); + int j = localCalendar2.get(1); + if (localCalendar1.get(1) != j) { + localCalendar1 = (Calendar) localCalendar1.clone(); + do { + i += localCalendar1.getActualMaximum(6); + localCalendar1.add(1, 1); + } while (localCalendar1.get(1) != j); + } + return i; + } + + public static Date addDays(Date paramDate, int paramInt) { + Calendar localCalendar = Calendar.getInstance(); + localCalendar.setTime(paramDate); + int i = localCalendar.get(6); + localCalendar.set(6, i + paramInt); + return localCalendar.getTime(); + } + + public static Date addDays(String paramString1, String paramString2, int paramInt) throws Exception { + Calendar localCalendar = Calendar.getInstance(); + Date localDate = stringToDate(paramString1, paramString2); + localCalendar.setTime(localDate); + int i = localCalendar.get(6); + localCalendar.set(6, i + paramInt); + return localCalendar.getTime(); + } + + public static Date addHours(Date paramDate, int paramInt) { + Calendar localCalendar = Calendar.getInstance(); + localCalendar.add(Calendar.HOUR_OF_DAY, paramInt); + return localCalendar.getTime(); + } + + public static java.sql.Date getSqlDate(Date paramDate) throws Exception { + java.sql.Date localDate = new java.sql.Date(paramDate.getTime()); + return localDate; + } + + public static String formatDate(Date paramDate, String parttern) { + if (paramDate == null) { + return null; + } + SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat(parttern); + localSimpleDateFormat.setLenient(false); + return localSimpleDateFormat.format(paramDate); + } + + public static String formatDate(Date paramDate) { + if (paramDate == null) { + return null; + } + localSimpleDateFormat.setLenient(false); + return localSimpleDateFormat.format(paramDate); + } + + public static String formatDateTime(Date paramDate) { + if (paramDate == null) { + return null; + } + SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + localSimpleDateFormat.setLenient(false); + return localSimpleDateFormat.format(paramDate); + } + + public static Date parse(String paramString, String pattern) throws Exception { + if (paramString == null || paramString.trim().equals("")) { + return null; + } + SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat(pattern); + localSimpleDateFormat.setLenient(false); + try { + return localSimpleDateFormat.parse(paramString); + } catch (ParseException localParseException) { + throw new Exception("日期解析出错!", localParseException); + } + } + + public static Date parseDate(String paramString) { + if (paramString == null || paramString.trim().equals("")) { + return null; + } + localSimpleDateFormat.setLenient(false); + try { + return localSimpleDateFormat.parse(paramString); + } catch (ParseException localParseException) { + log.error("===> parseDate error : ", localParseException); + } + return null; + } + + public static Date parseDateTime(String paramString) throws Exception { + if (paramString == null || paramString.trim().equals("")) { + return null; + } + SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + localSimpleDateFormat.setLenient(false); + try { + return localSimpleDateFormat.parse(paramString); + } catch (ParseException localParseException) { + throw new Exception("时间解析异常!", localParseException); + } + } + + public static Integer getYM(String paramString) throws Exception { + if (paramString == null) { + return null; + } + localSimpleDateFormat.setLenient(false); + Date localDate; + try { + localDate = localSimpleDateFormat.parse(paramString); + } catch (ParseException localParseException) { + throw new Exception("时间解析异常!", localParseException); + } + return getYM(localDate); + } + + public static Integer getYM(Date paramDate) { + if (paramDate == null) { + return null; + } + Calendar localCalendar = Calendar.getInstance(); + localCalendar.setTime(paramDate); + int i = localCalendar.get(1); + int j = localCalendar.get(2) + 1; + return new Integer(i * 100 + j); + } + + public static int addMonths(int paramInt1, int paramInt2) { + Calendar localCalendar = Calendar.getInstance(); + localCalendar.set(1, paramInt1 / 100); + localCalendar.set(2, paramInt1 % 100 - 1); + localCalendar.set(5, 1); + localCalendar.add(2, paramInt2); + return getYM(localCalendar.getTime()).intValue(); + } + + public static Date addMonths(Date paramDate, int paramInt) { + Calendar localCalendar = Calendar.getInstance(); + localCalendar.setTime(paramDate); + localCalendar.add(2, paramInt); + return localCalendar.getTime(); + } + + public static int monthsBetween(int paramInt1, int paramInt2) { + return (paramInt2 / 100 * 12 + paramInt2 % 100) + - (paramInt1 / 100 * 12 + paramInt1 % 100); + } + + public static int monthsBetween(Date paramDate1, Date paramDate2) { + return monthsBetween(getYM(paramDate1).intValue(), getYM(paramDate2).intValue()); + } + + public static String getChineseDate(Calendar paramCalendar) { + int i = paramCalendar.get(1); + int j = paramCalendar.get(2); + int k = paramCalendar.get(5); + StringBuffer localStringBuffer = new StringBuffer(); + localStringBuffer.append(i); + localStringBuffer.append("年"); + localStringBuffer.append(j + 1); + localStringBuffer.append("月"); + localStringBuffer.append(k); + localStringBuffer.append("日"); + return localStringBuffer.toString(); + } + + public static String getChineseWeekday(Calendar paramCalendar) { + switch (paramCalendar.get(Calendar.DAY_OF_WEEK)) { + case 2: + return "星期一"; + case 3: + return "星期二"; + case 4: + return "星期三"; + case 5: + return "星期四"; + case 6: + return "星期五"; + case 7: + return "星期六"; + case 1: + return "星期日"; + default: + return "未知"; + } + } + + /** + * 设置某天的23:59:59 + * + * @param date + * @return + */ + public static Date setLastTimeInOneDay(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.set(Calendar.HOUR_OF_DAY, 23); + cal.set(Calendar.MINUTE, 59); + cal.set(Calendar.SECOND, 59); + return cal.getTime(); + } + + /** + * 设置某天的00:00:00 + * + * @param date + * @return + */ + public static Date setFirstTimeInOneDay(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.set(Calendar.HOUR_OF_DAY, 00); + cal.set(Calendar.MINUTE, 00); + cal.set(Calendar.SECOND, 00); + cal.set(Calendar.MILLISECOND, 00); + return cal.getTime(); + } + + /** + * date 转 calendar + * + * @param date + * @return + */ + public static Calendar date2Cal(Date date) { + Calendar cal = Calendar.getInstance(); + try { + cal.setTime(parseDate(formatDate(date))); + } catch (Exception e) { + e.printStackTrace(); + } + return cal; + } + + public static String getUniqueTimeString() { + StringBuilder sb = new StringBuilder(); + sb.append(DateUtil.dateToString(new Date(), DateUtil.DETE_ALL_SIX)); + sb.append(String.valueOf((int) ((Math.random() * 9 + 1) * 100000))); + + return sb.toString(); + } + + /** + * 随机指定范围内N个不重复的数 最简单最基本的方法 + * + * @param min 指定范围最小值 + * @param max 指定范围最大值 + * @param n 随机数个数 + */ + public static int[] randomCommon(int min, int max, int n) { + if (n > (max - min + 1) || max < min) { + return null; + } + int[] result = new int[n]; + int count = 0; + while (count < n) { + int num = (int) (Math.random() * (max - min)) + min; + boolean flag = true; + for (int j = 0; j < n; j++) { + if (num == result[j]) { + flag = false; + break; + } + } + if (flag) { + result[count] = num; + count++; + } + } + return result; + } + + /** + * 获取两个时间的时间天数 + */ + public static long getDatePoorDay(Date endDate, Date nowDate) { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - nowDate.getTime(); + // 计算差多少天 + long day = diff / nd; + return day; + } + + /** + * 获取两个时间的时间查 如1天2小时30分钟 + */ + public static long getDatePoor(Date endDate, Date nowDate) { + + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - nowDate.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + long sec = diff % nd % nh % nm / ns; + // day + "天" + hour + "小时" + min + "分钟"+ sec; + return day * 24 * 60 + hour * 60 + min + (sec > 0 ? +1 : 0); + + } + + /** + * 计算两个时间差多少秒 + * @param start + * @param end + * @return + */ + public static long getDatePoorSecond(Date start, Date end) { + if(start == null || end == null) { + return 0; + } + long cs = start.getTime() - end.getTime(); + return cs > 0 ? cs / 1000 : 0; + } + + /** + * 计算两个日期主键分钟数 + * + * @param endDate 截止日期 + * @param beginDate 开始日期 + * @return 间隔分钟数 + */ + public static long getTimeMinutesLength(Date endDate, Date beginDate) { + if (endDate == null || beginDate == null) { + return 0; + } + + long len = endDate.getTime() - beginDate.getTime(); + // 1000 * 60 转换为分钟 + return len % 60000 == 0 ? len / 60000 : len / 60000 + 1; + } + + public static boolean isInDate(Date date, Date beginDate, Date endDate) { + if (date == null) { + return false; + } + if (beginDate == null && endDate == null) { + return false; + } + return (beginDate == null || date.after(beginDate)) + && (endDate == null || date.before(endDate)); + } + + public static boolean isSameDay(Date date1, Date date2) { + if (date1 == null || date2 == null) { + return false; + } + Calendar cal1 = Calendar.getInstance(); + cal1.setTime(date1); + + Calendar cal2 = Calendar.getInstance(); + cal2.setTime(date2); + if (cal1.get(Calendar.YEAR) != cal2.get(Calendar.YEAR)) { + return false; + } + if (cal1.get(Calendar.MONTH) != cal2.get(Calendar.MONTH)) { + return false; + } + if (cal1.get(Calendar.DAY_OF_MONTH) != cal2.get(Calendar.DAY_OF_MONTH)) { + return false; + } + return true; + } + + + public static Date dayStartDate(Date date) { + if (date == null) { + return null; + } + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c.getTime(); + } + + public static Date dayEndDate(Date date) { + if (date == null) { + return null; + } + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.set(Calendar.HOUR_OF_DAY, 23); + c.set(Calendar.MINUTE, 59); + c.set(Calendar.SECOND, 59); + c.set(Calendar.MILLISECOND, 999); + return c.getTime(); + } + + public static String getNow() { + return getNow(DETE_ALL_SIX); + } + + public static String getNow(String pattern) { + return new SimpleDateFormat(pattern).format(new Date()); + } + + + public static Long getIntervalSeconds(Date start, Integer intervalMin) { + if (start == null) { + return 0L; + } + long intervalSeconds = start.getTime() + intervalMin * 60 * 1000 - System.currentTimeMillis(); + return intervalSeconds > 0L ? intervalSeconds : 0L; + } + + public static Long getIntervalSeconds(Date start, Date end) { + if (start == null) { + return 0L; + } + long intervalSeconds = start.getTime() - end.getTime(); + return intervalSeconds > 0L ? intervalSeconds : 0L; + } + + /** + * 获取指定分钟后的时候 + * + * @param start 开始时间 + * @param min 多少分钟后 + * @return + */ + public static Long getMinAfterTime(Date start, Integer min) { + Long time = start.getTime() + (min * 60 * 1000); + return time; + } + + + /** + * 获取给定日期当前周的周一 00:00:00 + * + * @param date + * @return + */ + public static Date getMondayStart(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.setFirstDayOfWeek(Calendar.MONDAY); + calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + /** + * 是否是工作时间 + * 周中 8:00 - 21:00 ,周末 9:00 - 18:00为工作时间 + * + * @param date + * @return 上班时间:True 非上班时间:FALSE + */ + public static boolean isWorkTime(Date date) { + if (date == null) { + return false; + } + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); + int hour = calendar.get(Calendar.HOUR_OF_DAY); + if (dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY) { //周末 + return hour >= 9 && hour < 18; + } else { //周中 + return hour >= 8 && hour < 21; + } + + } + + /** + * HH:mm:ss to HH:mm + */ + public static String toHourMinute(String time) { + if (StringUtils.isBlank(time)) { + return null; + } + if (time.length() < 8) { + time += ":00"; + } + Time sqlTime = Time.valueOf(time); + return new SimpleDateFormat("HH:mm").format(sqlTime); + } + + /** + * HH:mm:ss to HH:mm + */ + public static String toHourMinute(Time time) { + if (time == null) { + return null; + } + return new SimpleDateFormat("HH:mm").format(time); + } + + /** + * 减少天数 + * + * @param date + * @param amount + * @return + */ + public static Date reduceDays(Date date, int amount) { + if (null == date) { + return null; + } + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + if (amount < 0) { + cal.add(Calendar.DATE, amount); + } else { + cal.add(Calendar.DATE, -amount); + } + return cal.getTime(); + } + + /** + * 增加天数 + * + * @param date + * @param interval + * @return + */ + public static Date addDay(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.DAY_OF_MONTH, interval); + return calendar.getTime(); + } + + public static List getDaysRage(Date startDate, Date endDate) { + List dates = new ArrayList<>(); + DateTime date1 = new DateTime(startDate); + DateTime date2 = new DateTime(endDate); + int num = Days.daysBetween(date1, date2).getDays(); + if (num < 0) { + return dates; + } + dates.add(startDate); + while (num > 0) { + date1 = date1.plusDays(1); + dates.add(date1.toDate()); + num--; + } + return dates; + } + + /** + * date2比date1多的天数 + * + * @param startDate + * @param endDate + * @return + */ + public static int getIntervalDays(Date startDate, Date endDate) { + return 1 + Days.daysBetween(new DateTime(DateUtil.formatDate(startDate, DateUtil.NORMAL_DATE_FORMAT)), new DateTime(DateUtil.formatDate(endDate, DateUtil.NORMAL_DATE_FORMAT))).getDays(); + } + + + public static String getZhHour(Date paramDate) { + DateTime dateTime = new DateTime(paramDate); + Integer hour = dateTime.getHourOfDay(); + if (hour > 12) { + return "下午" + (hour - 12) + "点"; + } else { + return "上午" + hour + "点"; + } + } + + /** + * 获取endDate与startDate日期相隔天数 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return + */ + public static Long differentDays(Date startDate, Date endDate) { + if (startDate == null || endDate == null) { + return 0L; + } + return (endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000); + } + + /** + * 获取endDate与startDate日期之间的日期列表,按pattern格式化 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param pattern 格式化规则 + * @return + */ + public static List differentDaysStr(Date startDate, Date endDate, String pattern) { + List daysList = new ArrayList<>(Arrays.asList(formatDate(startDate, pattern))); + long days = differentDays(startDate, endDate); + for (int i = 1; i < days + 1; i++) { + daysList.add(formatDate(addDay(startDate, i), pattern)); + } + return daysList; + } + + + /** + * 判断传入的时间是否与当前时间的年月日相等 + * @param target + * @return + */ + public static Boolean isEquals(Date target) { + String targetStr = dateToString(target, COMPACT_DATE_FORMAT); + String nowStr = dateToString(new Date(), COMPACT_DATE_FORMAT); + return targetStr.equals(nowStr); + } + + private static String getTimeInterval() throws ParseException { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + TimeZone time1 = TimeZone.getTimeZone("America/Los_Angeles"); + Calendar c = Calendar.getInstance(time1, Locale.US); + + Calendar cal = Calendar.getInstance(); + cal.setTime(c.getTime()); + // 判断要计算的日期是否是周日,如果是则减一天计算周六的,否则会出问题,计算到下一周去了 + int dayWeek = cal.get(Calendar.DAY_OF_WEEK);// 获得当前日期是一个星期的第几天 + if (1 == dayWeek) { + cal.add(Calendar.DAY_OF_MONTH, -1); + } + // System.out.println("要计算日期为:" + sdf.format(cal.getTime())); // 输出要计算日期 + // 设置一个星期的第一天 + cal.setFirstDayOfWeek(Calendar.SUNDAY); + // 获得当前日期是一个星期的第几天 + int day = cal.get(Calendar.DAY_OF_WEEK); + // 根据日历的规则,给当前日期减去星期几与一个星期第一天的差值 + cal.add(Calendar.DATE, cal.getFirstDayOfWeek() - day); + String imptimeBegin = sdf.format(cal.getTime()); +// System.out.println("所在周星期一的日期:" + imptimeBegin); + cal.add(Calendar.DATE, 6); + String imptimeEnd = sdf.format(cal.getTime()); +// System.out.println("所在周星期日的日期:" + imptimeEnd); + return imptimeBegin + "," + imptimeEnd; + } + + public static List findDates(Date dBegin, Date dEnd){ + List lDate = new ArrayList(); + lDate.add(dBegin); + Calendar calBegin = Calendar.getInstance(); + // 使用给定的 Date 设置此 Calendar 的时间 + calBegin.setTime(dBegin); + Calendar calEnd = Calendar.getInstance(); + // 使用给定的 Date 设置此 Calendar 的时间 + calEnd.setTime(dEnd); + // 测试此日期是否在指定日期之后 + while (dEnd.after(calBegin.getTime())) + { + // 根据日历的规则,为给定的日历字段添加或减去指定的时间量 + calBegin.add(Calendar.DAY_OF_MONTH, 1); + lDate.add(calBegin.getTime()); + } + return lDate; + } + + /** + * 根据传入的年月日返回日的天数,(主要是为了处理平年、闰年 2月的28天和29天的情况) + * @param year + * @param month + * @param day + * @return + */ + public static int getDay(int year, int month, int day) { + switch (month) { + case 2: + day = Math.min(day, IsoChronology.INSTANCE.isLeapYear(year) ? 29 : 28); + break; + } + return day; + } + + + public static void main(String[] args) throws Exception { +// List weekDateList = getWeekDateList(); +// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); +//// LocalDate inputDate = LocalDate.parse(); +//// 计算周开始时间 = 当前日期 - (当前日期是周几 - 周一)的日期差 +// TemporalAdjuster FIRST_OF_WEEK = TemporalAdjusters.ofDateAdjuster(localDate -> localDate.minusDays(localDate.getDayOfWeek().getValue()-DayOfWeek.MONDAY.getValue())); +// System.out.println(inputDate.with(FIRST_OF_WEEK)); +// +//// 计算周开始时间 = 当前日期 + (周日 - 当前日期是周几)的日期差 +// TemporalAdjuster LAST_OF_WEEK = TemporalAdjusters.ofDateAdjuster(localDate -> localDate.plusDays(DayOfWeek.SUNDAY.getValue() - localDate.getDayOfWeek().getValue())); +// System.out.println(inputDate.with(LAST_OF_WEEK)); + + } + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/DateUtils.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/DateUtils.java new file mode 100644 index 0000000..5cb9b9e --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/DateUtils.java @@ -0,0 +1,309 @@ +package com.sonic.frog.utils; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.google.common.base.Strings; +import org.apache.commons.lang3.time.FastDateFormat; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.*; + + +/** + * 日期工具类. + * + * @author xi.he + */ +public class DateUtils { + + public static final String Y_M_D_H_M_S = "yyyy-MM-dd HH:mm:ss"; + + public static final String Y_M_D = "yyyy-MM-dd"; + + public static final String YMDHMS = "yyyyMMddHHmmss"; + + public static final String YMD = "yyyyMMdd"; + + public static final String ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + + public static final long ONE_DAY_MillIS = 24 * 60 * 60 * 1000; + + public static final ZoneId ZONE_ID = ZoneId.systemDefault(); + + public static Date convert(Long timestamp) { + return new Date(timestamp); + } + + /** + * 方法描述:获取当前时间的 + */ + public static Date now() { + return new Date(); + } + + + public static String getNow(String pattern) { + return FastDateFormat.getInstance(pattern).format(new Date()); + } + + + public static Long currentTimeMillis() { + return System.currentTimeMillis(); + } + + /** + * 方法描述:格式化日期 + */ + public static String formatDate(Date d, String fmt) { + return FastDateFormat.getInstance(fmt).format(d); + } + + public static String formatY_M_D(Date d) { + return FastDateFormat.getInstance(Y_M_D).format(d); + } + + public static String formatY_M_D_H_M_S(Date d) { + return FastDateFormat.getInstance(Y_M_D_H_M_S).format(d); + } + + public static String formatYMD(Date d) { + return FastDateFormat.getInstance(YMD).format(d); + } + + public static String formatYMDHMS(Date d) { + return FastDateFormat.getInstance(YMDHMS).format(d); + } + + + public static String formatDate(Long date, String fmt) { + return FastDateFormat.getInstance(fmt).format(date); + } + + public static String format(Date date, String fmt) { + return FastDateFormat.getInstance(fmt).format(date); + } + + public static Date parse(String date, String fmt) { + try { + return FastDateFormat.getInstance(fmt).parse(date); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static Date parseY_M_D(String date) { + if (Strings.isNullOrEmpty(date)) { + return null; + } + try { + return FastDateFormat.getInstance(Y_M_D).parse(date); + } catch (ParseException e) { + e.printStackTrace(); + } + return null; + } + + public static Date parseY_M_D_H_M_S(String date) { + try { + return FastDateFormat.getInstance(Y_M_D_H_M_S).parse(date); + } catch (ParseException e) { + throw new IllegalArgumentException("the date pattern is error!"); + } + } + + public static Date parseISO8601(String dateText) { + DateTimeFormatter parser = ISODateTimeFormat.dateTimeNoMillis(); + DateTime dt = parser.parseDateTime(dateText); + return dt.toDate(); + } + + public static Date parseISO8601Mill(String dateText) { + DateTimeFormatter parser = ISODateTimeFormat.dateTime(); + DateTime dt = parser.parseDateTime(dateText); + return dt.toDate(); + } + + + public static Date parseYMD(String date) { + try { + return FastDateFormat.getInstance(YMD).parse(date); + } catch (ParseException e) { + e.printStackTrace(); + } + return null; + } + + public static Date parseYMDHMS(String date) { + try { + return FastDateFormat.getInstance(YMDHMS).parse(date); + } catch (ParseException e) { + throw new IllegalArgumentException("the date pattern is error!"); + } + } + + /** + * 是否是同一天 + */ + public static boolean isSameDay(Date date, Date date2) { + if (date == null || date2 == null) { + return false; + } + FastDateFormat df = FastDateFormat.getInstance(Y_M_D); + return df.format(date).equals(df.format(date2)); + } + + public static Date addMonth(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.MONTH, interval); + return calendar.getTime(); + } + + public static Date addDay(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.DAY_OF_MONTH, interval); + return calendar.getTime(); + } + + public static Date addHour(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.HOUR_OF_DAY, interval); + return calendar.getTime(); + } + + public static Date addMinute(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.MINUTE, interval); + return calendar.getTime(); + } + + public static Date addSecond(Date date, int interval) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.SECOND, interval); + return calendar.getTime(); + } + + /** + * 获取某一个日期的最小时间 + * @param date + * @return + */ + public static Date getDateMinTime(Date date){ + LocalDateTime endDateTime = LocalDateTime.ofInstant(date.toInstant(),ZONE_ID) + .with(LocalTime.MIN); + return Date.from(endDateTime.atZone(ZONE_ID).toInstant()); + } + /** + * 获取某一个日期的最大时间 + * @param date + * @return + */ + public static Date getDateMaxTime(Date date){ + LocalDateTime endDateTime = LocalDateTime.ofInstant(date.toInstant(),ZONE_ID) + .with(LocalTime.MAX); + return Date.from(endDateTime.atZone(ZONE_ID).toInstant()); + } + + /** + * 获取endDate与startDate日期相隔天数 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return + */ + public static Long differentDays(Date startDate,Date endDate) { + if (startDate == null || endDate == null) { + return 0L; + } + return (endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000); + } + + /** + * 获取endDate与startDate日期之间的日期列表,按pattern格式化 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param pattern 格式化规则 + * @return + */ + public static List differentDaysStr(Date startDate, Date endDate,String pattern) { + List daysList = new ArrayList<>(Arrays.asList(DateUtils.format(startDate,pattern))); + long days = differentDays(startDate,endDate); + for (int i = 1; i < days+1; i++) { + daysList.add(format(addDay(startDate,i),pattern)); + } + return daysList; + } + + public static List getDays(String startTime, String endTime) { + List result = new ArrayList(); + try { + if (StringUtils.isEmpty(startTime) || StringUtils.isEmpty(endTime)) { + return null; + } + //1、定义转换格式 + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + + Date start = df.parse(startTime); + Date end = df.parse(endTime); + + Calendar tempStart = Calendar.getInstance(); + tempStart.setTime(start); + Calendar tempEnd = Calendar.getInstance(); + tempEnd.setTime(end); + + result.add(startTime); + tempStart.add(Calendar.DAY_OF_YEAR, 1); + while (tempStart.before(tempEnd)) { + result.add(df.format(tempStart.getTime())); + tempStart.add(Calendar.DAY_OF_YEAR, 1); + } + if (!startTime.equals(endTime)) { + result.add(endTime); + } + } catch (ParseException e) { + e.printStackTrace(); + } + return result; + } + + + + public static Long getIntervalSeconds(Date start, Integer intervalMin) { + if (start == null) { + return 0L; + } + long intervalSeconds = start.getTime() + intervalMin * 60 * 1000 - System.currentTimeMillis(); + return intervalSeconds > 0L ? intervalSeconds : 0L; + } + + public static void main(String[] args){ + String text = "2020-02-08T15:20:19Z"; + DateTimeFormatter parser = ISODateTimeFormat.dateTimeNoMillis(); + DateTime dt = parser.parseDateTime(text); + DateTimeFormatter formatter = DateTimeFormat.mediumDateTime(); + System.out.println(formatter.print(dt)); + } + + public static String toFormatDate(String strDate) { + try { + Date date = new SimpleDateFormat("yyyyMMdd").parse(strDate); + return new SimpleDateFormat("yyyy-MM-dd").format(date); + } catch (ParseException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/HttpHeaderUtils.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/HttpHeaderUtils.java new file mode 100644 index 0000000..cae25f3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/HttpHeaderUtils.java @@ -0,0 +1,21 @@ +package com.sonic.frog.utils; + +import javax.servlet.http.HttpServletRequest; + +/** + * @Author code + * @Description 工具类 + * @Version 1.0 + */ +public class HttpHeaderUtils { + + /** + * 获取UserAgent + * @param request + * @return + */ + public static String getUserAgent(HttpServletRequest request) { + return request.getHeader("user-agent"); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/KeyGenerator.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/KeyGenerator.java new file mode 100644 index 0000000..265ced6 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/KeyGenerator.java @@ -0,0 +1,29 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.sonic.frog.utils; + +import java.util.UUID; + +public class KeyGenerator extends AbstractKeyGenerator { + private static final KeyGenerator SINGLE = new KeyGenerator(); + + private KeyGenerator() { + } + + public static KeyGenerator instance() { + return SINGLE; + } + + public static String UUID() { + return UUID.randomUUID().toString(); + } + + @Override + public String customKey() { + return ""; + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/LimitUtils.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/LimitUtils.java new file mode 100644 index 0000000..3b7486c --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/LimitUtils.java @@ -0,0 +1,87 @@ +package com.sonic.frog.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 限流工具类 + * @Author zzhan + * @Date 2020/4/17 16:35 + * @Version 1.0 + */ +@Slf4j +@Component +public class LimitUtils { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * redis key不存在的过期时间值 + */ + private Integer NULL_KEY_EXP_TIME_VALUE = -2; + + /** + * redis key永不过期的过期时间值 + */ + private Integer NEVER_EXPIRES_VALUE = -1; + + /** + * 根据请求参数进行限流检查 + * @param redisKey 自定义封装的redisKey + * @param count 限流的数量 + * @param time 时间段:单位为秒 + */ + public boolean defaultLimitCheckByKey(String redisKey, int count, int time) { + //用户操作某个话题的点赞,超过4次就提示,锁定5s + int num; + Long expTime = stringRedisTemplate.getExpire(redisKey); + if (expTime == null || expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", time, TimeUnit.SECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(redisKey, 1).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis并返回false + stringRedisTemplate.delete(redisKey); + return false; + } + if (num > count) { + log.info("===>超过了限定的次数[" + count + "]"); + return true; + } + return false; + } + + /** + * 根据请求参数进行限流检查 + * @param redisKey 自定义封装的redisKey + * @param time 时间段:单位为秒 + */ + public int defaultLimitCheckReturnCount(String redisKey, int time) { + //用户操作某个话题的点赞,超过4次就提示,锁定5s + int num; + Long expTime = stringRedisTemplate.getExpire(redisKey); + if (expTime == null || expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", time, TimeUnit.SECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(redisKey, 1).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis并返回false + stringRedisTemplate.delete(redisKey); + return 0; + } + return num; + } + + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/MD5Util.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/MD5Util.java new file mode 100644 index 0000000..095cce3 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/MD5Util.java @@ -0,0 +1,69 @@ +package com.sonic.frog.utils; + +import org.apache.commons.codec.digest.DigestUtils; + +import java.io.UnsupportedEncodingException; +import java.security.SignatureException; + +/** + * md5加密 + * + * @author Xi.He + * + */ +public class MD5Util { + + /** + * 加密字符串(大写) + * @param text 需要加密的字符串 + * @param charset 编码格式 + * @return 签名结果 + */ + public static String upperDigest(String text, String charset) { + return DigestUtils.md5Hex(getContentBytes(text, charset)).toUpperCase(); + } + + /** + * 加密字符串(小写) + * @param text 需要加密的字符串 + * @param charset 编码格式 + * @return 签名结果 + */ + public static String digest(String text, String charset) { + return DigestUtils.md5Hex(getContentBytes(text, charset)); + } + + + /** + * 加密字符串(小写) + * @param text 需要加密的字符串 + * @return 签名结果 + */ + public static String digest(String text) { + return DigestUtils.md5Hex(getContentBytes(text, "UTF-8")); + } + + + /** + * @param content 内容 + * @param charset 编码 + * @return byte[] + * @throws SignatureException + * @throws UnsupportedEncodingException + */ + private static byte[] getContentBytes(String content, String charset) { + if (charset == null || "".equals(charset)) { + return content.getBytes(); + } + try { + return content.getBytes(charset); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + // ExceptionUtils.throwPipelineError(true, + // ResponseCode.PIPELINE_MD5_SIGN_ERROR, charset); + } + } + public static void main(String[] args) { + System.out.println(digest(digest("Danong2018"))); + } +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/MaskUtils.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/MaskUtils.java new file mode 100644 index 0000000..3da78a6 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/MaskUtils.java @@ -0,0 +1,75 @@ +package com.sonic.frog.utils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +@Slf4j +public class MaskUtils { + + /** + * 隐藏手机号 + * + * @param mobile + * @return + */ + public static String maskMobile(String mobile) { + if (StringUtils.isEmpty(mobile) || (mobile.length() != 11)) { + return mobile; + } + return mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); + } + + /** + * 隐藏手机号 + * + * @param mobile + * @return + */ + public static String maskMobile2(String mobile) { + if (StringUtils.isEmpty(mobile)) { + return mobile; + } + if (mobile.startsWith("+")) { + mobile = mobile.substring(1); + } + if (mobile.length() == 7) { + return mobile.replaceAll("(\\d{3})(\\d{4})", "***$2"); + } + return mobile.replaceAll("(\\d{0,7})(\\d{4})(\\d{4})", "$1****$3"); + } + + /** + * 隐藏邮箱信息 + * + * @param email + * @return + */ + public static String maskEmail(String email) { + if (StringUtils.isEmpty(email)) return ""; + + String prefix = ""; + try { + if (email.indexOf("@") > 5) { + prefix = email.substring(0, 3); + } else { + prefix = email.substring(0, 1); + } + + String middle = ""; + if (prefix.length() < email.indexOf("@")) { + for (int i = prefix.length(); i < email.indexOf("@"); i++) { + middle += "*"; + } + } + + String suffix = email.substring(email.indexOf("@")); + return prefix + middle + suffix; + } catch (Exception e) { + log.error( + String.format("email encrypt fail, param:%s, errorMsg:%s", email, e.getMessage()), e + ); + return ""; + } + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/RedisKeyUtils.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/RedisKeyUtils.java new file mode 100644 index 0000000..a5f52f5 --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/RedisKeyUtils.java @@ -0,0 +1,373 @@ +package com.sonic.frog.utils; + +import com.alibaba.fastjson.JSON; +import com.sonic.common.AppRuntime; +import com.sonic.frog.enums.AgeTypeEnum; +import com.sonic.frog.enums.DeductionTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 统一的 redis key 管理工具类(方便后续的维护和查找) + * + * @Author zzhan + * @Date 2021/9/24 + * @Version 1.0 + */ +@Slf4j +@Service +public class RedisKeyUtils { + + @Autowired + private AppRuntime appRuntime; + @Value("${spring.profiles.active}") + private String runMode; + + + /** + * AI字典信息缓存RedisKey + * + * @return + */ + public String aiDictCacheKey() { + return appRuntime.buildPrefixKey("aiDictCache"); + } + + /** + * AI音色字典缓存RedisKey + * + * @return + */ + public String aiTimbreDictCacheKey() { + return appRuntime.buildPrefixKey("aiTimbreDictCache", "v2"); + } + + /** + * AI用户创建或编辑锁RedisKey + * + * @param userId + * @return + */ + public String aiUserCreateEditLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "aiUserCreateEdit", userId); + } + + /** + * 背景添加锁RedisKey + * + * @param userId + * @return + */ + public String backgroundAddLockKey(Long userId) { + return appRuntime.buildPrefixKey("background", "add", userId); + } + + /** + * AI用户基础信息key 在sonic_cow服务中使用,构建聊天提示词,图片生成提示词等 + * + * @param aiId + * @return + */ + public String aiUserCacheInfoKey(Long aiId) { + return "aiUserCacheInfo" + aiId; + } + + /** + * 关系等级缓存 + * + * @param userId + * @param aiId + * @return + */ + public String aiUserHeartbeatLevelCacheKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("cache", "heartbeatLevel", userId, aiId); + } + + + /** + * 用户购买心动值锁RedisKey + * + * @param userId + * @return + */ + public String buyHeartbeatValLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "buyHeartbeatVal", userId); + } + + /** + * 用户发送礼物锁RedisKey + * + * @param userId + * @return + */ + public String sendGiftLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "sendGift", userId); + } + + /** + * 计算用户心动等级锁RedisKey + * + * @param userId + * @param aiId + * @return + */ + public String calcHeartbeatLevelLockKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("lock", "calcHeartbeatLevel", userId); + } + + /** + * 计算用户心动等级锁RedisKey + * + * @param userId + * @return + */ + public String calcHeartbeatRankLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "calcHeartbeatRank", userId); + } + + /** + * 计算用户心动分数限流RedisKey + * + * @param userId + * @param aiId + * @return + */ + public String calcHeartbeatScoreLimitKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("limit", "calcHeartbeatScore", userId, aiId); + } + + /** + * 和AI聊天时的上下文缓存ID(eg: 1010:chat:responseId:xx:xx) + * + * @param userId + * @param aiId + * @return + */ + public String chatResponseIdCacheKey(Long userId, Long aiId) { + if (runMode.equals("test")) { + return "1010:test:chat:responseId:" + userId + ":" + aiId; + } + return "1010:chat:responseId:" + userId + ":" + aiId; + } + + /** + * 和AI聊天时的上下文缓存清理任务(eg: 1010:chat:responseId:clear:xx:xx) + * + * @param userId + * @param aiId + * @return + */ + public String chatResponseIdClearTaskKey(Long userId, Long aiId) { + if (runMode.equals("test")) { + return "1010:test:chat:responseId:clear:" + userId + ":" + aiId; + } + return "1010:chat:responseId:clear:" + userId + ":" + aiId; + } + + /** + * ai聊天会话次数key + * 每20次更新到数据库,同时清0 + * + * @return + */ + public String aiChatNumKey() { + return appRuntime.buildPrefixKey("aiChatNum"); + } + + /** + * 用户心动值总排行榜key + * + * @return + */ + public String heartbeatValTotalRankKey() { + return appRuntime.buildPrefixKey("rank", "heartbeatValTotal"); + } + + /** + * meet滑动请求次数 + * + * @param userId + * @return + */ + public String meetSdCount(Long userId) { + return appRuntime.buildPrefixKey("meetSdCount", userId); + } + + /** + * meet成功的概率 + * + * @param userId + * @return + */ + public String meetRateBoKey(Long userId) { + return appRuntime.buildPrefixKey("meetRateBo", userId); + } + + /** + * 限流 + * + * @param userId + * @return + */ + public String meetRcLimit(Long userId) { + return appRuntime.buildPrefixKey("limit", "meetRc", userId); + } + + /** + * 当前用户是否有上报meet成功的次数 + * + * @param userId + * @param aiId + * @return + */ + public String uploadMeet(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("uploadMeet", userId, aiId); + } + + /** + * 签到时使用的redis锁 + * + * @param userId + * @return + */ + public String signInLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "signIn", userId); + } + + /** + * 首页不同分类缓存RedisKey + * + * @param codeStr + * @return + */ + public String homeClassificationCacheKey(String codeStr, List sexList, List ageList, int minLiked, int maxLiked) { + return appRuntime.buildPrefixKey("homeClassificationCache", codeStr, JSON.toJSONString(sexList), JSON.toJSONString(ageList), minLiked, maxLiked); + } + + /** + * 热聊榜单 + * + * @return + */ + public String chatRankKey() { + return appRuntime.buildPrefixKey("rank", "chat"); + } + + /** + * 心动榜单 + * + * @return + */ + public String aiHeartbeatRankKey() { + return appRuntime.buildPrefixKey("rank", "aiHeartbeat"); + } + + /** + * 礼物榜单 + * + * @return + */ + public String aiGiftRankKey() { + return appRuntime.buildPrefixKey("rank", "gift"); + } + + /** + * 购买创建图片次数锁RedisKey + * + * @param userId + * @return + */ + public String buyCreateImageCountLockKey(Long userId) { + return appRuntime.buildPrefixKey("buyCreateImageCount", userId); + } + + /** + * 解锁爱慕者锁RedisKey + * + * @param userId + * @return + */ + public String unlockLikeYouLockKey(Long userId) { + return appRuntime.buildPrefixKey("unlockLikeYou", userId); + } + + /** + * 用户文本,语音,语音通话预扣款统计锁RedisKey + * + * @param userId + * @param aiId + * @param deductionType + */ + public String userDeductionsStatLockKey(Long userId, Long aiId, DeductionTypeEnum deductionType) { + return appRuntime.buildPrefixKey("userDeductionsStat", userId, aiId, deductionType.name()); + } + + /** + * meet上报的锁 + * + * @param userId + * @return + */ + public String meetSdLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "meetSd", userId); + } + + /** + * 用户余额不足时,检查 checkout + * + * @param userId + * @return + */ + public String userBalanceInsufficientCheckoutLockKey(Long userId) { + return appRuntime.buildPrefixKey("userBalanceInsufficientCheckout", userId); + } + + /** + * 相册图片解锁锁键 + * + * @param userId + * @return + */ + public String unlockAlbumImgLockKey(Long userId, Long albumId) { + return appRuntime.buildPrefixKey("unlockAlbumImg", userId, albumId); + } + + /** + * ai轮播列表缓存RedisKey + */ + public String aiCarouselListKey() { + return appRuntime.buildPrefixKey("aiCarouselList"); + } + + /** + * 更新最后聊天时间判断创建人关系 + * + * @param userId + * @return + */ + public String creatorAiUserRelationKey(Long userId, Long aiId) { + return appRuntime.buildPrefixKey("limit", "updateLastChatTime", userId, aiId); + } + + /** + * 更新最后一次聊天时间 + * @param aiId + * @return + */ + public String updateLastChatTimeLimitKey(Long aiId) { + return appRuntime.buildPrefixKey("limit", "updateLastChatTime", aiId); + } + + /** + * 首页数据缓存 + * @return + */ + public String homeRecommendV2CacheKey() { + return appRuntime.buildPrefixKey("recommend", "cache", "v2", "data"); + } + +} diff --git a/sonic-frog/server/src/main/java/com/sonic/frog/utils/RegexUtil.java b/sonic-frog/server/src/main/java/com/sonic/frog/utils/RegexUtil.java new file mode 100644 index 0000000..c873d5b --- /dev/null +++ b/sonic-frog/server/src/main/java/com/sonic/frog/utils/RegexUtil.java @@ -0,0 +1,72 @@ +package com.sonic.frog.utils; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +/** + * 格式验证工具 + */ +public class RegexUtil { + + /** + * TODO 待完善 + */ + private static List ossStartPatternList = new ArrayList<>(Arrays.asList( + "https://hhb.crushlevel.ai/" + )); + + /** + * 验证Email + * + * @param email email地址,格式:zhangsan@zuidaima.com,zhangsan@xxx.com.cn,xxx代表邮件服务商 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkEmail(String email) { + if (StringUtils.isEmpty(email)){ + return false; + } + String regex = "[A-Za-z\\d]+([-_.][A-Za-z\\d]+)*@([A-Za-z\\d]+[-.])+[A-Za-z\\d]{2,4}"; + return Pattern.matches(regex, email); + } + + + /** + * 判断字符串是否为URL + * @param url 需要判断的String类型url + * @return true:是URL;false:不是URL + */ + public static boolean isHttpUrl(String url) { + boolean isurl = false; + String regex = "http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?";//设置正则表达式 + isurl = Pattern.matches(regex.trim(),url);//判断是否匹配 + if (isurl) { + isurl = true; + } + return isurl; + } + + /** + * 判断字符串是否为OSS域名开头的URL(校验URL格式) + * @param url 需要判断的String类型url + */ + public static boolean isOssStartUrl(String url) { + if(StringUtils.isBlank(url)){ + return false; + } + boolean isOssUrl = false; + if(isHttpUrl(url)){ + for (String ossStartPattern: ossStartPatternList) { + if(url.startsWith(ossStartPattern)){ + isOssUrl = true; + break; + } + } + } + return isOssUrl; + } + +} diff --git a/sonic-frog/server/src/main/resources/application-dev.yml b/sonic-frog/server/src/main/resources/application-dev.yml new file mode 100644 index 0000000..62c183e --- /dev/null +++ b/sonic-frog/server/src/main/resources/application-dev.yml @@ -0,0 +1,55 @@ +spring: + datasource: + # TODO: 需要改写为Dev环境的Mysql地址, 数据库名称,用户名和密码 + url: jdbc:mysql://54.223.196.180:3306/sonic-frog?useSSL=false&autoReconnect=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull + username: root + password: toukagames1234 + + redis: + # TODO: 需要改写为DEV环境的Redis地址, 数据库名称和密码 + host: 54.223.196.180 + port: 6379 + database: 0 + password: 123456 + # cluster: + # nodes: 192.168.100.238 + # ssl: false + + rabbitmq: + # TODO: 需要改写为dev环境的rabbitmq地址, 用户名和密码 + host: 54.223.196.180 + port: 5672 + username: guest + password: toukagames1234 + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#三方登录相关配置 +thirdLogin: + #google邮箱登录 + gmail: + client: + webId: xxxxxx + iosId: xxxxxxxx + androidId: xxxxxxxx + authUrl: https://oauth2.googleapis.com/tokeninfo?id_token= + #Discord登录 + discord: + clientID: 1396735872459866233 + clientSecret: ujcuP-TMVU3XltqLo2AxFtDqXaE_6vz_ + redirectUri: http://localhost:3000/api/auth/discord/callback + scope: identify email rpc + #苹果登录 + apple: + clientId: gg.sonic + redirectUri: https://abc.com/user/apple/code + aud: com.sonic.bs + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bs.controller" \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/application-local.yml b/sonic-frog/server/src/main/resources/application-local.yml new file mode 100644 index 0000000..00130f3 --- /dev/null +++ b/sonic-frog/server/src/main/resources/application-local.yml @@ -0,0 +1,55 @@ +spring: + datasource: + # TODO: 需要改写为Dev环境的Mysql地址, 数据库名称,用户名和密码 + url: jdbc:mysql://192.168.100.238:3306/sonic-frog?useSSL=false&autoReconnect=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull + username: egirl_dev + password: lpkq609oI9eRc + + redis: + # TODO: 需要改写为DEV环境的Redis地址, 数据库名称和密码 + host: 192.168.100.238 + port: 6379 + database: 0 + password: Epal@2020 +# cluster: +# nodes: 192.168.100.238 +# ssl: false + + rabbitmq: + # TODO: 需要改写为dev环境的rabbitmq地址, 用户名和密码 + host: 192.168.100.238 + port: 5672 + username: guest + password: epal@2020 + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#三方登录相关配置 +thirdLogin: + #google邮箱登录 + gmail: + client: + webId: xxxxxx + iosId: xxxxxxxx + androidId: xxxxxxxx + authUrl: https://oauth2.googleapis.com/tokeninfo?id_token= + #Discord登录 + discord: + clientID: 1396735872459866233 + clientSecret: 23f41cbef6294995f55e50a7212f3e7f58b1fe68c68af4a635e6c2bf7aee9b53 + redirectUri: https://gamersegirl.com/link/auth + scope: identify email rpc + #苹果登录 + apple: + clientId: gg.sonic + redirectUri: https://abc.com/user/apple/code + aud: com.sonic.bs + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bs.controller" \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/application-product.yml b/sonic-frog/server/src/main/resources/application-product.yml new file mode 100644 index 0000000..2dd65c9 --- /dev/null +++ b/sonic-frog/server/src/main/resources/application-product.yml @@ -0,0 +1,62 @@ +spring: + datasource: + url: ${DB.MASTER.FROG.URL} + username: ${DB.MASTER.USERNAME} + password: ${DB.MASTER.PASSWORD} + + redis: +# host: ${REDIS.MAIN.HOST} +# port: ${REDIS.MAIN.PORT} +# database: 0 +# password: ${REDIS.MAIN.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + + rabbitmq: + host: ${MQ.HOST} + port: ${MQ.PORT} + username: ${MQ.USERNAME} + password: ${MQ.PASSWORD} + ssl: + enabled: true + +web: + frequency-alert: + # TODO 修改接口访问频率告警配置。 + # 默认配置含义为: /* 所有接口5秒访问500次. + rules: + - /*:5/500 + +#三方登录相关配置 +thirdLogin: + #google邮箱登录 + gmail: + client: + webId: ${THIRD_LOGIN.GMAIL.CLIENT.WEB_ID} + iosId: ${THIRD_LOGIN.GMAIL.CLIENT.IOS_ID} + androidId: ${THIRD_LOGIN.GMAIL.CLIENT.ANDROID_ID} + authUrl: https://oauth2.googleapis.com/tokeninfo?id_token= + #Discord登录 + discord: + clientID: ${THIRD_LOGIN.DISCORD.CLIENT_ID} + clientSecret: ${THIRD_LOGIN.DISCORD.CLIENT_SECRET} + redirectUri: ${THIRD_LOGIN.DISCORD.REDIRECT_URI} + scope: identify email rpc + #苹果登录 + apple: + clientId: ${THIRD_LOGIN.APPLE.CLIENT_ID} + redirectUri: ${THIRD_LOGIN.APPLE.REDIRECT_URI} + aud: ${THIRD_LOGIN.APPLE.AUD} + +#swagger展示相关的配置 +swagger: + enabled: false + base: + package: "com.sonic.bs.controller" + +#配置cdn签名的基础配置 +cloudFront: + privateKey: + url: ${CLOUD_FRONT.PRIVATE_KEY.URL} + keyPairId: + url: ${CLOUD_FRONT.KEY_PAIR_ID.URL} \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/application-test.yml b/sonic-frog/server/src/main/resources/application-test.yml new file mode 100644 index 0000000..3b0611e --- /dev/null +++ b/sonic-frog/server/src/main/resources/application-test.yml @@ -0,0 +1,60 @@ +spring: + datasource: + url: ${DB.MASTER.FROG.URL} + username: ${DB.MASTER.USERNAME} + password: ${DB.MASTER.PASSWORD} + + redis: +# host: ${REDIS.MAIN.HOST} +# port: ${REDIS.MAIN.PORT} +# database: 0 +# password: ${REDIS.MAIN.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + + rabbitmq: + host: ${MQ.HOST} + port: ${MQ.PORT} + username: ${MQ.USERNAME} + password: ${MQ.PASSWORD} + ssl: + enabled: true + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#三方登录相关配置 +thirdLogin: + #google邮箱登录 + gmail: + client: + webId: ${THIRD_LOGIN.GMAIL.CLIENT.WEB_ID} + iosId: ${THIRD_LOGIN.GMAIL.CLIENT.IOS_ID} + androidId: ${THIRD_LOGIN.GMAIL.CLIENT.ANDROID_ID} + authUrl: https://oauth2.googleapis.com/tokeninfo?id_token= + #Discord登录 + discord: + clientID: ${THIRD_LOGIN.DISCORD.CLIENT_ID} + clientSecret: ${THIRD_LOGIN.DISCORD.CLIENT_SECRET} + redirectUri: ${THIRD_LOGIN.DISCORD.REDIRECT_URI} + scope: identify email rpc + #苹果登录 + apple: + clientId: ${THIRD_LOGIN.APPLE.CLIENT_ID} + redirectUri: ${THIRD_LOGIN.APPLE.REDIRECT_URI} + aud: ${THIRD_LOGIN.APPLE.AUD} + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bs.controller" + +#配置cdn签名的基础配置 +cloudFront: + privateKey: + url: ${CLOUD_FRONT.PRIVATE_KEY.URL} + keyPairId: + url: ${CLOUD_FRONT.KEY_PAIR_ID.URL} \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/application.yml b/sonic-frog/server/src/main/resources/application.yml new file mode 100644 index 0000000..99cb938 --- /dev/null +++ b/sonic-frog/server/src/main/resources/application.yml @@ -0,0 +1,119 @@ +spring: + profiles: + # profile目前支持以下5种:local/unittest/dev/test/product + # 开发的时候一般使用dev或者local + # 在测试环境/生产环境,该配置不起作用,会被外部传入的jvm启动参数(spring.profiles.active)或者环境变量覆盖 + active: dev + application: + # TODO: 更换项目名称 + name: frog + # 必须使用引号,否则会转成8进制 + id: 1002 + task: + execution: + pool: + max-size: 50 + core-size: 4 + queue-capacity: 20480 + keep-alive: 30s + # TODO: 如果不需要mysql,请移除datasource相关的所有配置 + datasource: + driver-class-name: com.mysql.jdbc.Driver + hikari: + auto-commit: true + connection-timeout: 20000 + maximum-pool-size: 30 + minimum-idle: 5 + idle-timeout: 300000 + max-lifetime: 1200000 + # TODO: 如果不需要Redis,请移除redis相关的所有配置 + redis: + lettuce: + pool: + max-active: 1000 + max-wait: 1000 + max-idle: 100 + + rabbitmq: + listener: + simple: + acknowledge-mode: manual + concurrency: 2 + max-concurrency: 10 + #限流 + prefetch: 1 + +# TODO: 如果不需要mysql,请移除mybatis-plus相关的所有配置 +mybatis-plus: + # 定义mybatis映射文件的位置 + mapper-locations: classpath:/mapper/*Mapper.xml + +web: + frequency-alert: + # TODO 修改接口访问频率告警配置, 如果不配置的FrequencyAlertInterceptor不会生效. + # 以下配置含义为: /* 1秒访问20次, /index 10秒访问100次. 访问频率超过该规则会触发告警consumer + rules: + - /*:1/20 + - /index:10/100 + +mq: + # 如无必要,无须修改 exchange + exchange: message-server-exchange + default: + # TODO: {Event.BuildInScene.code}-{appName}-queue + queue: bs-demo-queue + # TODO: {Event.BuildInScene.code}-{appName}-routing-key + routing-key: bs-demo-routing-key + # AI IM基础信息队列 + aiImInfo: + queue: ai-im-info-queue + routing-key: ai-im-info-routing-key + # 心动等级计算队列 + calc-heartbeat-level: + queue: calc-heartbeat-level-queue + routing-key: calc-heartbeat-level-routing-key + calc-heartbeat-rank: + queue: calc-heartbeat-rank-queue + routing-key: calc-heartbeat-rank-routing-key + #AI聊天的队列 + aiChat: + queue: ai-chat-queue + routing-key: ai-chat-routing-key + #AI聊天同步业务服务 + aiChatToFrog: + queue: ai-chatToFrog-queue + routing-key: ai-chatToFrog-routing-key + # 24小时未聊天扣减心动值队列(打开app或进入网站时拉接口发送MQ处理) + subtract-heartbeat-val: + queue: subtract-heartbeat-val-queue + routing-key: subtract-heartbeat-val-routing-key + # 计算心动分(IM聊天详情拉接口时发送MQ处理) + calc-heartbeat-score: + queue: calc-heartbeat-score-queue + routing-key: calc-heartbeat-score-routing-key + # ai统计数据MQ + ai-user-stat: + queue: ai-user-stat-queue + routing-key: ai-user-stat-routing-key + # 文本,语音,语音通话预扣款统计 + user-deduction-stat: + queue: user-deduction-stat-queue + routing-key: user-deduction-stat-routing-key + #文本,语音,语音通话预扣款,如果余额不足,发起扣款 + user-balance-insufficient-checkout: + queue: user-balance-insufficient-checkout-queue + routing-key: user-balance-insufficient-checkout-routing-key + #ai编辑,修改,删除更新首页缓存 + ai-change: + queue: ai-change-queue + routing-key: ai-change-routing-key +# swagger 默认开启,在生产环境关闭,节省资源 +swagger: + enabled: true + + +# 禁用健康检查 +management: + endpoints: + enabled-by-default: false #关闭监控 + diff --git a/sonic-frog/server/src/main/resources/logback-spring.xml b/sonic-frog/server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b8dd4d1 --- /dev/null +++ b/sonic-frog/server/src/main/resources/logback-spring.xml @@ -0,0 +1,62 @@ + + + + + + + + + + ${DefaultPattern} + + + + + + + + ${ColorfulPattern} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sonic-frog/server/src/main/resources/mapper/AdvertiseMapper.xml b/sonic-frog/server/src/main/resources/mapper/AdvertiseMapper.xml new file mode 100644 index 0000000..8235f65 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/AdvertiseMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/AiDictMapper.xml b/sonic-frog/server/src/main/resources/mapper/AiDictMapper.xml new file mode 100644 index 0000000..6c41c61 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/AiDictMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/AiUserAlbumMapper.xml b/sonic-frog/server/src/main/resources/mapper/AiUserAlbumMapper.xml new file mode 100644 index 0000000..724aa76 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/AiUserAlbumMapper.xml @@ -0,0 +1,56 @@ + + + + + + UPDATE ai_user_album set + liked_count = if(ifnull(liked_count, 0) + #{count} > 0, ifnull(liked_count, 0) + #{count}, 0) + WHERE id = #{id}; + + + + + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/AiUserExtMapper.xml b/sonic-frog/server/src/main/resources/mapper/AiUserExtMapper.xml new file mode 100644 index 0000000..7cb7d06 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/AiUserExtMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/AiUserGiftMapper.xml b/sonic-frog/server/src/main/resources/mapper/AiUserGiftMapper.xml new file mode 100644 index 0000000..484c10f --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/AiUserGiftMapper.xml @@ -0,0 +1,25 @@ + + + + + + + + INSERT INTO ai_user_gift + (ai_id, gift_id, num,create_time) + VALUES (#{aiId}, #{giftId}, #{num},now()) + ON DUPLICATE KEY UPDATE + num = num + VALUES(num), + edit_time = now() + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/AiUserHeartbeatRelationMapper.xml b/sonic-frog/server/src/main/resources/mapper/AiUserHeartbeatRelationMapper.xml new file mode 100644 index 0000000..cf73d82 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/AiUserHeartbeatRelationMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/AiUserMapper.xml b/sonic-frog/server/src/main/resources/mapper/AiUserMapper.xml new file mode 100644 index 0000000..19466b1 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/AiUserMapper.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/AiUserStatMapper.xml b/sonic-frog/server/src/main/resources/mapper/AiUserStatMapper.xml new file mode 100644 index 0000000..0266d59 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/AiUserStatMapper.xml @@ -0,0 +1,128 @@ + + + + + update ai_user_stat + set liked_num = if(liked_num + #{num} > 0, liked_num + #{num}, 0) + where ai_id = #{aiId} + + + + update ai_user_stat + set disliked_num = if(disliked_num + #{num} > 0, disliked_num + #{num}, 0) + where ai_id = #{aiId} + + + + update ai_user_stat + set chat_num = chat_num + #{num} + where ai_id = #{aiId} + + + + update ai_user_stat + set conversation_num = conversation_num + 1 + where ai_id = #{aiId} + + + + update ai_user_stat + set coin_num = coin_num + #{coinNum} + where ai_id = #{aiId} + + + + update ai_user_stat + set gift_coin_num = gift_coin_num + #{coinNum} + where ai_id = #{aiId} + + + + update ai_user_stat + set unlock_img_coin_num = unlock_img_coin_num + #{coinNum} + where ai_id = #{aiId} + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/BuyCreateCountRecordMapper.xml b/sonic-frog/server/src/main/resources/mapper/BuyCreateCountRecordMapper.xml new file mode 100644 index 0000000..efb1334 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/BuyCreateCountRecordMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/BuyHeartbeatValueRecordMapper.xml b/sonic-frog/server/src/main/resources/mapper/BuyHeartbeatValueRecordMapper.xml new file mode 100644 index 0000000..309f6fb --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/BuyHeartbeatValueRecordMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/ChatBubbleDictMapper.xml b/sonic-frog/server/src/main/resources/mapper/ChatBubbleDictMapper.xml new file mode 100644 index 0000000..1320b26 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/ChatBubbleDictMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/ChatModelDictMapper.xml b/sonic-frog/server/src/main/resources/mapper/ChatModelDictMapper.xml new file mode 100644 index 0000000..3bcf35c --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/ChatModelDictMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/ChatSetMapper.xml b/sonic-frog/server/src/main/resources/mapper/ChatSetMapper.xml new file mode 100644 index 0000000..e404f53 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/ChatSetMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/ChatUserBackgroundMapper.xml b/sonic-frog/server/src/main/resources/mapper/ChatUserBackgroundMapper.xml new file mode 100644 index 0000000..04255a6 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/ChatUserBackgroundMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/GiftDictMapper.xml b/sonic-frog/server/src/main/resources/mapper/GiftDictMapper.xml new file mode 100644 index 0000000..b00c8ab --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/GiftDictMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/GiftRewardRecordMapper.xml b/sonic-frog/server/src/main/resources/mapper/GiftRewardRecordMapper.xml new file mode 100644 index 0000000..177dec3 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/GiftRewardRecordMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/HeartbeatLevelDictMapper.xml b/sonic-frog/server/src/main/resources/mapper/HeartbeatLevelDictMapper.xml new file mode 100644 index 0000000..7b6bde7 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/HeartbeatLevelDictMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/ImageStyleDictDao.xml b/sonic-frog/server/src/main/resources/mapper/ImageStyleDictDao.xml new file mode 100644 index 0000000..fa0bce1 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/ImageStyleDictDao.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/LikedMapper.xml b/sonic-frog/server/src/main/resources/mapper/LikedMapper.xml new file mode 100644 index 0000000..204ef35 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/LikedMapper.xml @@ -0,0 +1,21 @@ + + + + + + INSERT INTO `liked` ( + `biz_id`, + `biz_type`, + `ai_id`, + `liked_user_id`, + `liked_status`) + VALUES ( + #{bizId}, + #{bizType}, + #{aiId}, + #{likedUserId}, + #{likedStatus}) + ON DUPLICATE KEY UPDATE + liked_status = #{likedStatus} + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/TimbreDictMapper.xml b/sonic-frog/server/src/main/resources/mapper/TimbreDictMapper.xml new file mode 100644 index 0000000..d666446 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/TimbreDictMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/UserCreateCountStatMapper.xml b/sonic-frog/server/src/main/resources/mapper/UserCreateCountStatMapper.xml new file mode 100644 index 0000000..1efd838 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/UserCreateCountStatMapper.xml @@ -0,0 +1,19 @@ + + + + + + update user_create_count_stat + set buy_num = buy_num + #{count},edit_time=now() + where user_id = #{userId} + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/mapper/UserDeductionStatMapper.xml b/sonic-frog/server/src/main/resources/mapper/UserDeductionStatMapper.xml new file mode 100644 index 0000000..b1e35a1 --- /dev/null +++ b/sonic-frog/server/src/main/resources/mapper/UserDeductionStatMapper.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/messages.properties b/sonic-frog/server/src/main/resources/messages.properties new file mode 100644 index 0000000..7c50ce3 --- /dev/null +++ b/sonic-frog/server/src/main/resources/messages.properties @@ -0,0 +1,43 @@ +SYS_SYSTEM_EXCEPTION=System error +SYS_PARAMETERS_VALIDATE_EXCEPTION=Parameter validation error +NO_LOGIN=User is not logged in +USER_NOT_EXIST=User does not exist +APP_CLIENT_NOT_EXIST=Login client does not exist +APP_CLIENT_NOT_ALLOW=Not authorized to log in to the client +SESSION_EXPIRED=Session is expired +SESSION_INVALID=Session is invalid +PASSWORD_INVALID=Invalid account or password +AUTH_FAIL=Authorization failed +USER_FROZEN=User is frozen +ACCOUNT_DOES_NOT_EXIST=The account does not exist +AGE_CHECK=User age cannot be less than 18 years old +USER_NICKNAME_EXIST=User nickname already exists +SYS_PERMISSION_DENIED=Insufficient permissions +AI_USER_NOT_EXIST=AI user does not exist +ALBUM_IS_DELETED=Image has been deleted +ALBUM_NOT_EXIST=Image does not exist +ALBUM_DEFAULT_CAN_NOT_DELETE=Default image cannot be deleted +ALBUM_ENCRYPT_CAN_NOT_LIKE=Encrypted image cannot be liked +ALBUM_DEFAULT_CAN_NOT_MODIFY_PAY=Default image cannot be changed to paid +ALBUM_ADD_MAX_ONE=Can add up to one image +GEN_IMAGE_LIMIT_ERROR=Maximum of 10 image changes within 24 hours, please try again in {0} hours and {1} minutes +CHAT_BUBBLE_NOT_EXIST=Chat bubble does not exist +BACKGROUND_IS_DELETED=Background has been deleted +BACKGROUND_NOT_EXIST=Background does not exist +BACKGROUND_ADD_MAX_ONE=Can add up to one background +GIFT_NOT_EXIST=Gift does not exist +NOT_MEMBER_CREATE_AI_ONE=Non-members can only create 1 AI +MEMBER_CREATE_AI_MAX_NUM=Members can create up to 5 AIs +USER_CREATE_COUNT_NONE=No creation attempts remaining +PARAM_NOT_NULL=Parameter cannot be empty +PARAM_LEN_MIN_ERROR=Length is insufficient +SYS_VALIDATION_FAILED_ERROR=Validation failed, invalid request +ACCOUNT_NOT_REGISTER=Incorrect username or password +FACEBOOK_ACCOUNT_ERROR=Facebook account could not be verified +FACEBOOK_INVALID=Facebook login is invalid, please login again +APPLE_NETWORK_ERROR=Apple network error +MISS_PARAM_ERROR=Missing parameter +DISCORD_NETWORK_ERROR=Discord network error +GOOGLE_ID_GET_ERROR=Google ID get error +DEVICE_BLOCK_ERROR=This device has been banned +FREEZE_ERROR=Account has been frozen \ No newline at end of file diff --git a/sonic-frog/server/src/main/resources/messages_en.properties b/sonic-frog/server/src/main/resources/messages_en.properties new file mode 100644 index 0000000..7c50ce3 --- /dev/null +++ b/sonic-frog/server/src/main/resources/messages_en.properties @@ -0,0 +1,43 @@ +SYS_SYSTEM_EXCEPTION=System error +SYS_PARAMETERS_VALIDATE_EXCEPTION=Parameter validation error +NO_LOGIN=User is not logged in +USER_NOT_EXIST=User does not exist +APP_CLIENT_NOT_EXIST=Login client does not exist +APP_CLIENT_NOT_ALLOW=Not authorized to log in to the client +SESSION_EXPIRED=Session is expired +SESSION_INVALID=Session is invalid +PASSWORD_INVALID=Invalid account or password +AUTH_FAIL=Authorization failed +USER_FROZEN=User is frozen +ACCOUNT_DOES_NOT_EXIST=The account does not exist +AGE_CHECK=User age cannot be less than 18 years old +USER_NICKNAME_EXIST=User nickname already exists +SYS_PERMISSION_DENIED=Insufficient permissions +AI_USER_NOT_EXIST=AI user does not exist +ALBUM_IS_DELETED=Image has been deleted +ALBUM_NOT_EXIST=Image does not exist +ALBUM_DEFAULT_CAN_NOT_DELETE=Default image cannot be deleted +ALBUM_ENCRYPT_CAN_NOT_LIKE=Encrypted image cannot be liked +ALBUM_DEFAULT_CAN_NOT_MODIFY_PAY=Default image cannot be changed to paid +ALBUM_ADD_MAX_ONE=Can add up to one image +GEN_IMAGE_LIMIT_ERROR=Maximum of 10 image changes within 24 hours, please try again in {0} hours and {1} minutes +CHAT_BUBBLE_NOT_EXIST=Chat bubble does not exist +BACKGROUND_IS_DELETED=Background has been deleted +BACKGROUND_NOT_EXIST=Background does not exist +BACKGROUND_ADD_MAX_ONE=Can add up to one background +GIFT_NOT_EXIST=Gift does not exist +NOT_MEMBER_CREATE_AI_ONE=Non-members can only create 1 AI +MEMBER_CREATE_AI_MAX_NUM=Members can create up to 5 AIs +USER_CREATE_COUNT_NONE=No creation attempts remaining +PARAM_NOT_NULL=Parameter cannot be empty +PARAM_LEN_MIN_ERROR=Length is insufficient +SYS_VALIDATION_FAILED_ERROR=Validation failed, invalid request +ACCOUNT_NOT_REGISTER=Incorrect username or password +FACEBOOK_ACCOUNT_ERROR=Facebook account could not be verified +FACEBOOK_INVALID=Facebook login is invalid, please login again +APPLE_NETWORK_ERROR=Apple network error +MISS_PARAM_ERROR=Missing parameter +DISCORD_NETWORK_ERROR=Discord network error +GOOGLE_ID_GET_ERROR=Google ID get error +DEVICE_BLOCK_ERROR=This device has been banned +FREEZE_ERROR=Account has been frozen \ No newline at end of file diff --git a/sonic-gateway/.gitignore b/sonic-gateway/.gitignore new file mode 100644 index 0000000..51bb6a0 --- /dev/null +++ b/sonic-gateway/.gitignore @@ -0,0 +1,26 @@ +/**/rebel.xml + +#target/ +target/ + +# IDEA # +.idea/ +*.iml + +# Eclipse # +.settings/ +.classpath +.project + +# Log # +logs/ +*.log% + +#mac +.DS_Store + +#svn +.svn +#reviewboardrc +.reviewboardrc +**/rest-client.private.env.json diff --git a/sonic-gateway/README.md b/sonic-gateway/README.md new file mode 100644 index 0000000..dce9b7d --- /dev/null +++ b/sonic-gateway/README.md @@ -0,0 +1,2 @@ +# gateway + diff --git a/sonic-gateway/pom.xml b/sonic-gateway/pom.xml new file mode 100644 index 0000000..dd795b1 --- /dev/null +++ b/sonic-gateway/pom.xml @@ -0,0 +1,177 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.4 + + + com.games + sonic-gateway + 1.0 + sonic-gateway + sonic gateway + + + 1.8 + 2020.0.3 + 2.5.4 + + + + + com.mashape.unirest + unirest-java + 1.4.9 + + + + com.github.ben-manes.caffeine + caffeine + 2.6.0 + + + + com.google.guava + guava + 28.0-jre + + + + org.bouncycastle + bcprov-jdk16 + 1.46 + + + + org.apache.httpcomponents + httpclient + 4.5.7 + + + + + log4j-api + org.apache.logging.log4j + 2.17.0 + + + + org.redisson + redisson + 3.16.2 + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + org.springframework.boot + spring-boot-starter-test + test + + + logback-classic + ch.qos.logback + + + logback-core + ch.qos.logback + + + + + org.projectlombok + lombok + + + org.springframework.boot + spring-boot-starter-data-redis-reactive + + + + commons-io + commons-io + 2.11.0 + + + + org.apache.commons + commons-lang3 + 3.9 + + + + + com.aliyun + alibaba-dingtalk-service-sdk + 2.0.0 + + + log4j + log4j + + + + + + logback-classic + ch.qos.logback + 1.2.12 + + + logback-core + ch.qos.logback + + + + + logback-core + ch.qos.logback + 1.2.12 + + + + com.alibaba + fastjson + 1.2.83 + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/sonic-gateway/src/main/java/com/games/gateway/Application.java b/sonic-gateway/src/main/java/com/games/gateway/Application.java new file mode 100644 index 0000000..d633482 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/Application.java @@ -0,0 +1,14 @@ +package com.games.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/client/SsoTouchSessionRpcClient.java b/sonic-gateway/src/main/java/com/games/gateway/client/SsoTouchSessionRpcClient.java new file mode 100644 index 0000000..3bb4652 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/client/SsoTouchSessionRpcClient.java @@ -0,0 +1,10 @@ +package com.games.gateway.client; + +import com.games.gateway.domain.Session; + +public interface SsoTouchSessionRpcClient { + + + Session touchSession(final String token); + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/client/impl/SsoTouchSessionRpcClientImpl.java b/sonic-gateway/src/main/java/com/games/gateway/client/impl/SsoTouchSessionRpcClientImpl.java new file mode 100644 index 0000000..f20ff17 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/client/impl/SsoTouchSessionRpcClientImpl.java @@ -0,0 +1,98 @@ +package com.games.gateway.client.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.games.gateway.client.SsoTouchSessionRpcClient; +import com.games.gateway.config.GatewayBizConfig; +import com.games.gateway.domain.Session; +import com.games.gateway.enums.BizResultCode; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.base.Strings; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class SsoTouchSessionRpcClientImpl implements SsoTouchSessionRpcClient, InitializingBean { + + /** + * URL地址 + */ + private static final String URI_TOUCH_SESSION = "/api/auth/touch-session"; + + /** + * 本地缓存 + */ + private LoadingCache localSessionCache; + + /** + * 过期时间 + */ + private long localSessionAliveMills = 1 * 1000; + /** + * 压缩session. 不保存tk信息. 这样可以减少cache的大小 + */ + private boolean isSessionCompacted = true; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private GatewayBizConfig globalConfig; + + /** + * 正常返回则表示该session处于活跃状态;抛出异常表示该会话已过期。 + *

+ * LoadingCache.get,如果缓存中不存在,则调用load直接访问远程服务,并放入本地缓存。 + * 如果 load 抛出异常,get 会直接抛出 load 的异常 + *

+ * + * @param token 用户会话TOKEN + */ + public Session touchSession(final String token) { + BizResultCode.NO_LOGIN.check(Strings.isNullOrEmpty(token)); + + Session session = localSessionCache.get(token); + BizResultCode.NO_LOGIN.check(session == null); + + if (session != null) { + session.setToken(token); + } + return session; + } + + + @Override + public void afterPropertiesSet() { + localSessionCache = Caffeine.newBuilder() + .expireAfterWrite(localSessionAliveMills, TimeUnit.MILLISECONDS) + .initialCapacity(10_000) + .maximumSize(1_000_000) + .recordStats() + .build(key -> { + Map param = new HashMap<>(2); + param.put("token", key); + // 从SSO拿到session对象并保存到本地缓存,如果缓存无效或过期,则抛出异常 + String response = restTemplate.postForObject(globalConfig.getBearHost() + URI_TOUCH_SESSION, + param, String.class); + JSONObject jsonObject = JSON.parseObject(response); + String sessionJson = jsonObject.getString("content"); + Session session = null; + if (sessionJson != null) { + session = JSON.parseObject(sessionJson, Session.class); + if (isSessionCompacted) { + session.setToken(null); + } + } + return session; + }); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/config/CorsConfig.java b/sonic-gateway/src/main/java/com/games/gateway/config/CorsConfig.java new file mode 100644 index 0000000..fa43904 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/config/CorsConfig.java @@ -0,0 +1,31 @@ +package com.games.gateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * 跨域配置 + */ +@Configuration +public class CorsConfig { + + @Bean + public CorsWebFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); + source.registerCorsConfiguration("/**", buildConfig()); + return new CorsWebFilter(source); + } + + private CorsConfiguration buildConfig() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //在生产环境上最好指定域名,以免产生跨域安全问题 + corsConfiguration.addAllowedOrigin("*"); + corsConfiguration.addAllowedHeader("*"); + corsConfiguration.addAllowedMethod("*"); + return corsConfiguration; + } +} \ No newline at end of file diff --git a/sonic-gateway/src/main/java/com/games/gateway/config/FilterOrderedConfig.java b/sonic-gateway/src/main/java/com/games/gateway/config/FilterOrderedConfig.java new file mode 100644 index 0000000..107186a --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/config/FilterOrderedConfig.java @@ -0,0 +1,16 @@ +package com.games.gateway.config; + +/** + * 排序 Filter 的执行顺序 数字越大 优先级越低。 + * 所以 是 按 优先级从高到底排序的。 + */ +public interface FilterOrderedConfig { + int PrintRequestFilter = 1; // 打印请求日志 + int IpLimitGlobalFilter = 2; // IP 限流 + int IpPathLimitGlobalFilter = 3; // IP 限流 + int PathLimitGlobalFilter = 4; // 路径限流 + int TokenLimitGlobalFilter = 5; // 用户Token 限流 + int URLBlackListGlobalFilter = 6; // 请求404 URL黑名单 + int AuthFilter = 7; // 用户权限过滤 + int ResponseHeaderModifyFilter = 9; // 修改返回最终给 客户端请求头 +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/config/GatewayBizConfig.java b/sonic-gateway/src/main/java/com/games/gateway/config/GatewayBizConfig.java new file mode 100644 index 0000000..4889d66 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/config/GatewayBizConfig.java @@ -0,0 +1,48 @@ +package com.games.gateway.config; + +import lombok.Data; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Data +@ConfigurationProperties(prefix = "gateway") +public class GatewayBizConfig { + /** + * ip+path 24小时内最大访问次数 + */ + private Integer ipPath24HoursLimiter; + /** + * IP限流 每秒令牌数 + */ + private Integer ipLimiter; + /** + * path限流 每秒令牌数 + */ + private Integer pathLimiter; + /** + * token限流 每秒令牌数 + */ + private Integer tokenLimiter; + + /** + * 频繁token撞库攻击 ip限制1小时限制数 + */ + private Integer tokenCsIpLimiter; + /** + * sso 调用地址 + */ + private String bearHost; + /** + * 部署的内部服务名 列表 + */ + private List applicationNames; +} \ No newline at end of file diff --git a/sonic-gateway/src/main/java/com/games/gateway/config/GlobalExceptionHandler.java b/sonic-gateway/src/main/java/com/games/gateway/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..bae04f5 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/config/GlobalExceptionHandler.java @@ -0,0 +1,77 @@ +package com.games.gateway.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.games.gateway.domain.Result; +import com.games.gateway.exceptions.BizException; +import com.games.gateway.utils.TraceUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.handler.ResponseStatusExceptionHandler; +import reactor.core.publisher.Mono; + +/** + * 网关异常通用处理器,只作用在webflux 环境下 , 优先级低于 {@link ResponseStatusExceptionHandler} 执行 + * + * @author 冷酱 + * @date 2020/5/26 + */ +@Component +@Slf4j +@Order(-1) +@RequiredArgsConstructor +public class GlobalExceptionHandler implements ErrorWebExceptionHandler { + + private final ObjectMapper objectMapper; + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) { + ServerHttpResponse response = exchange.getResponse(); + + if (response.isCommitted()) { + return Mono.error(ex); + } + + // header set + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + if (ex instanceof ResponseStatusException) { + response.setStatusCode(((ResponseStatusException) ex).getStatus()); + } + + return response.writeWith(Mono.fromSupplier(() -> { + DataBufferFactory bufferFactory = response.bufferFactory(); + try { + log.error("发生系统异常",ex); + if(ex instanceof BizException){ + BizException currentEx = (BizException)ex; + Result result = new Result( currentEx.getErrorCode(),currentEx.getErrorMsg()); + result.setTraceId(TraceUtils.getTraceId()); + TraceUtils.removeTraceId(); + return bufferFactory.wrap(objectMapper.writeValueAsBytes(result)); + }else if(ex instanceof ResponseStatusException ){ + Result result = new Result("",ex.getMessage(),Result.Status.ERROR); + result.setTraceId(TraceUtils.getTraceId()); + TraceUtils.removeTraceId(); + return bufferFactory.wrap(objectMapper.writeValueAsBytes(result)); + }else { + Result result = new Result("","system error",Result.Status.ERROR); + result.setTraceId(TraceUtils.getTraceId()); + TraceUtils.removeTraceId(); + return bufferFactory.wrap(objectMapper.writeValueAsBytes(result)); + } + } + catch (JsonProcessingException e) { + log.error("Error writing response", ex); + return bufferFactory.wrap(new byte[0]); + } + })); + } +} \ No newline at end of file diff --git a/sonic-gateway/src/main/java/com/games/gateway/config/RestTemplateConfig.java b/sonic-gateway/src/main/java/com/games/gateway/config/RestTemplateConfig.java new file mode 100644 index 0000000..4b529e6 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/config/RestTemplateConfig.java @@ -0,0 +1,74 @@ +package com.games.gateway.config; + +import org.apache.http.client.HttpClient; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; +@Configuration +public class RestTemplateConfig { + + /** + * http连接管理器 + * @return + */ + @Bean + public HttpClientConnectionManager poolingHttpClientConnectionManager() { + PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(); + // 最大连接数 + poolingHttpClientConnectionManager.setMaxTotal(500); + // 同路由并发数(每个主机的并发) + poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100); + return poolingHttpClientConnectionManager; + } + + /** + * HttpClient + * @param poolingHttpClientConnectionManager + * @return + */ + @Bean + public HttpClient httpClient(HttpClientConnectionManager poolingHttpClientConnectionManager) { + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); + // 设置http连接管理器 + httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager); + return httpClientBuilder.build(); + } + + /** + * 请求连接池配置 + * @param httpClient + * @return + */ + @Bean + public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) { + HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); + // httpClient创建器 + clientHttpRequestFactory.setHttpClient(httpClient); + // 连接超时时间/毫秒(连接上服务器(握手成功)的时间,超出抛出connect timeout) + clientHttpRequestFactory.setConnectTimeout(5 * 1000); + // 数据读取超时时间(socketTimeout)/毫秒(务器返回数据(response)的时间,超过抛出read timeout) + clientHttpRequestFactory.setReadTimeout(10 * 1000); + // 连接池获取请求连接的超时时间,不宜过长,必须设置/毫秒(超时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool) + clientHttpRequestFactory.setConnectionRequestTimeout(10 * 1000); + return clientHttpRequestFactory; + } + + /** + * rest模板 + * @return + */ + @Bean + public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) { + // boot中可使用RestTemplateBuilder.build创建 + RestTemplate restTemplate = new RestTemplate(); + // 配置请求工厂 + restTemplate.setRequestFactory(clientHttpRequestFactory); + return restTemplate; + } + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/decorator/RecorderServerHttpRequestDecorator.java b/sonic-gateway/src/main/java/com/games/gateway/decorator/RecorderServerHttpRequestDecorator.java new file mode 100644 index 0000000..c5258b4 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/decorator/RecorderServerHttpRequestDecorator.java @@ -0,0 +1,36 @@ +package com.games.gateway.decorator; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j//lombok插件 +public class RecorderServerHttpRequestDecorator extends ServerHttpRequestDecorator { + + private final List dataBuffers = new ArrayList<>(); + + public RecorderServerHttpRequestDecorator(ServerHttpRequest delegate) { + super(delegate); + super.getBody().map(dataBuffer -> { + dataBuffers.add(dataBuffer); + return dataBuffer; + }).subscribe(); + } + + @Override + public Flux getBody() { + return copy(); + } + + private Flux copy() { + return Flux.fromIterable(dataBuffers) + .map(buf -> buf.factory().wrap(buf.asByteBuffer())); + } + + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/domain/IpPathLimit.java b/sonic-gateway/src/main/java/com/games/gateway/domain/IpPathLimit.java new file mode 100644 index 0000000..bb75b3d --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/domain/IpPathLimit.java @@ -0,0 +1,28 @@ +package com.games.gateway.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: ip+path限制 + * @author: mzc + * @date: 2022-09-16 16:24 + **/ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class IpPathLimit { + + /** + * 限制访问次数 + */ + private Integer count; + + /** + * 限制时间 + */ + private Integer expireTime; +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/domain/Result.java b/sonic-gateway/src/main/java/com/games/gateway/domain/Result.java new file mode 100644 index 0000000..2d96f40 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/domain/Result.java @@ -0,0 +1,93 @@ +package com.games.gateway.domain; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class Result implements Serializable { + private static final long serialVersionUID = 5925101851082556646L; + /** + * 数据对象 + */ + private T content; + /** + * 状态:OK|ERROR + */ + private String status; + /** + * 错误码 + */ + private String errorCode; + /** + * 错误消息 + */ + private String errorMsg; + + private String traceId; + + public Result() { + this.status = Status.SUCCESS.code(); + } + + public Result(String errorCode, String errorMsg) { + this(errorCode, errorMsg, Status.ERROR); + } + + public Result(String errorCode, String errorMsg, Status status) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + this.status = status.code(); + } + + public T getContent() { + return this.content; + } + + public Result setContent(T content) { + this.content = content; + return this; + } + + public String getStatus() { + return this.status; + } + + public Result setStatus(String status) { + this.status = status; + return this; + } + + public String getErrorCode() { + return this.errorCode; + } + + public Result setErrorCode(String errorCode) { + this.errorCode = errorCode; + return this; + } + + public String getErrorMsg() { + return this.errorMsg; + } + + public Result setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + return this; + } + + public static enum Status { + SUCCESS("OK"), + ERROR("ERROR"); + + private String code; + + private Status(String code) { + this.code = code; + } + + public String code() { + return this.code; + } + } +} \ No newline at end of file diff --git a/sonic-gateway/src/main/java/com/games/gateway/domain/ServiceAuthRegist.java b/sonic-gateway/src/main/java/com/games/gateway/domain/ServiceAuthRegist.java new file mode 100644 index 0000000..dc5a472 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/domain/ServiceAuthRegist.java @@ -0,0 +1,26 @@ +package com.games.gateway.domain; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 服务授权注册 + */ +@Data +public class ServiceAuthRegist implements Serializable { + + /** + * 服务名 对应 spring.application.name + */ + private String applicationName; + /** + * 无需授权的URL + */ + private List ignoreAuthList; + /** + * URL 黑名单 + */ + private List urlBlacklist; +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/domain/ServiceAuthRegisterRequest.java b/sonic-gateway/src/main/java/com/games/gateway/domain/ServiceAuthRegisterRequest.java new file mode 100644 index 0000000..bf4c413 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/domain/ServiceAuthRegisterRequest.java @@ -0,0 +1,8 @@ +package com.games.gateway.domain; + +import lombok.Data; + +@Data +public class ServiceAuthRegisterRequest { + private String content; +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/domain/Session.java b/sonic-gateway/src/main/java/com/games/gateway/domain/Session.java new file mode 100644 index 0000000..f2afd14 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/domain/Session.java @@ -0,0 +1,93 @@ +package com.games.gateway.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import com.games.gateway.enums.AccountType; +import com.games.gateway.enums.SessionStatusEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author code + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Session { + + /** + * 授权token + */ + private String token; + /** + * 用户ID + */ + private Long userId; + /** + * session状态 + */ + private SessionStatusEnum status; + /** + * session创建时间 + */ + private LocalDateTime createTime; + /** + * session最后访问时间 + */ + private LocalDateTime lastAccessTime; + /** + * session过期时间 + */ + private LocalDateTime expireTime; + /** + * 用户注册时间 + */ + private LocalDateTime registerTime; + /** + * 终端类型【前端透传 WEB、ANDROID、IOS】 + */ + private String endpoint; + + /** + * 数字版本号【前端透传 数字版本号】 + */ + private Integer versionNum; + + /** + * 终端设备唯一识别码【前端透传】 + */ + private String deviceCode; + + /** + * 终端设备IP【前端透传】 + */ + private String ip; + + /** + * 终端设备UA【前端透传】 + */ + private String userAgent; + + @JSONField(serialize = false) + public boolean isExpired() { + return expireTime != null && LocalDateTime.now().isAfter(expireTime); + } + + /** + * 是否超过最大存活期内 + * + * @param sessionAlive 最大存活时间,单位分钟,由 client 定义 + * @return 是否在最大存活时间内 + */ + @JSONField(serialize = false) + public boolean isOverMaxAlive(Integer sessionAlive) { + LocalDateTime maxAliveTime = createTime.plusMinutes(sessionAlive); + return LocalDateTime.now().isAfter(maxAliveTime); + } + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/enums/AccountType.java b/sonic-gateway/src/main/java/com/games/gateway/enums/AccountType.java new file mode 100644 index 0000000..bed9184 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/enums/AccountType.java @@ -0,0 +1,36 @@ +package com.games.gateway.enums; + +import lombok.Getter; + +/** + * 账号类型 + * @author code + */ +@Getter +public enum AccountType { + /** + * 客户用户 + */ + CUSTOMER(1), + /** + * 后台账号 + */ + SYSTEM(2), + ; + + private final Integer code; + + AccountType(Integer code) { + this.code = code; + } + + public static AccountType from(Integer code) { + for (AccountType status : AccountType.values()) { + if (code.equals(status.code)) { + return status; + } + } + + return null; + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/enums/BizResultCode.java b/sonic-gateway/src/main/java/com/games/gateway/enums/BizResultCode.java new file mode 100644 index 0000000..f860038 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/enums/BizResultCode.java @@ -0,0 +1,58 @@ +package com.games.gateway.enums; + +import com.games.gateway.exceptions.BizException; +import com.games.gateway.utils.MessageUtils; +import lombok.Getter; + +/** + * @description: 弹窗消息的定义 + * @author: zzhan + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum BizResultCode { + + NO_LOGIN("0001", "User is not logged in"), + DEVICE_ID_ERROR("0002", "NOT DEVICE ID"), + SYS_OPERATOR_FAST_ERROR("", "请求太频繁"), + //2022-11-24 临时处理,后面删掉 等android重新发布后直接将这行代码废弃掉 + APP_VERSION_ERROR("0001", ""), + + ; + + + private final String errorCode; + private final String errorMsg; + + BizResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + public String getAppId() { + return "1005"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + throw new BizException(this.getErrorCode(), MessageUtils.get(this.name())); + } + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/enums/LimiterTypeEnum.java b/sonic-gateway/src/main/java/com/games/gateway/enums/LimiterTypeEnum.java new file mode 100644 index 0000000..703bd41 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/enums/LimiterTypeEnum.java @@ -0,0 +1,16 @@ +package com.games.gateway.enums; + +import lombok.Getter; + +/** + * @description: + * @author: mzc + * @date: 2022-09-27 11:17 + **/ +@Getter +public enum LimiterTypeEnum { + TOKEN, + IP, + PATH, + ; +} \ No newline at end of file diff --git a/sonic-gateway/src/main/java/com/games/gateway/enums/SessionStatusEnum.java b/sonic-gateway/src/main/java/com/games/gateway/enums/SessionStatusEnum.java new file mode 100644 index 0000000..7e25d35 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/enums/SessionStatusEnum.java @@ -0,0 +1,21 @@ +package com.games.gateway.enums; + +/** + * @Author zzhan + * @Description 用户 session 状态,默认为有效 + * @Date 2023/11/10 15:56 + * @Version 1.0 + */ +public enum SessionStatusEnum { + + /** 有效 */ + ENABLED, + /** 已过期 */ + EXPIRED, + /** 已登出 */ + LOGOUT, + /** 踢下线 */ + KICK_OUT + ; + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/exceptions/BizException.java b/sonic-gateway/src/main/java/com/games/gateway/exceptions/BizException.java new file mode 100644 index 0000000..1c15715 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/exceptions/BizException.java @@ -0,0 +1,27 @@ +package com.games.gateway.exceptions; + +public class BizException extends RuntimeException { + private final String errorCode; + private final String errorMsg; + + + public BizException(String errorCode, String errorMsg) { + super(String.format("BizException{errorCode:%s, errorMsg:%s}", errorCode, errorMsg)); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + public BizException(String errorCode, String errorMsg, Throwable cause) { + super(String.format("BizException{errorCode:%s, errorMsg:%s}", errorCode, errorMsg), cause); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + public String getErrorCode() { + return this.errorCode; + } + + public String getErrorMsg() { + return this.errorMsg; + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/filters/AuthFilter.java b/sonic-gateway/src/main/java/com/games/gateway/filters/AuthFilter.java new file mode 100644 index 0000000..be65237 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/filters/AuthFilter.java @@ -0,0 +1,120 @@ +package com.games.gateway.filters; + +import com.alibaba.fastjson.JSON; +import com.games.gateway.client.SsoTouchSessionRpcClient; +import com.games.gateway.config.FilterOrderedConfig; +import com.games.gateway.domain.Session; +import com.games.gateway.enums.BizResultCode; +import com.games.gateway.utils.AuthCache; +import com.games.gateway.utils.Constant; +import com.games.gateway.utils.MessageUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.CollectionUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.List; + + +/** + * 权限过滤器 + */ +@Order(value = FilterOrderedConfig.AuthFilter) +@Slf4j +@Component +public class AuthFilter implements GlobalFilter { + + @Autowired + private AuthCache authCache; + + @Autowired + private SsoTouchSessionRpcClient ssoTouchSessionRpcClient; + + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + // 不需要授权的URL 放权 + String path = exchange.getRequest().getURI().getPath(); + AntPathMatcher antPathMatcher = new AntPathMatcher(); + boolean isIgnoreAuth = false; + + //获取服务名称 + String applicationName = exchange.getRequest().getHeaders().getFirst(Constant.APPLICATION_NAME); + List ignoreAuthPathList = authCache.getIgnoreAuthPathList(applicationName); + log.debug("===> path :{}, ignoreAuthPathList : {}", path, ignoreAuthPathList); + if (!CollectionUtils.isEmpty(ignoreAuthPathList)) { + for (String nologinUri : ignoreAuthPathList) { + path = path.replace("/", ""); + nologinUri = nologinUri.replace("/", ""); + if (antPathMatcher.match(nologinUri, path)) { + isIgnoreAuth = true; + break; + } + } + } + + //不管是否 需要登陸 都要 查詢一下 session + extractedSession(exchange, isIgnoreAuth); + return chain.filter(exchange); + } + + /** + * 提取session进行验证 + * @param exchange + * @param isIgnoreAuth + */ + private void extractedSession(ServerWebExchange exchange, boolean isIgnoreAuth) { + //获取token + String token = exchange.getRequest().getHeaders().getFirst(Constant.AUTH_TK); + //设置locale + MessageUtils.setLocale(exchange); + + Session session = null; + log.info("extractedSession token:{}", token); + if (StringUtils.isBlank(token) && !isIgnoreAuth) { + BizResultCode.NO_LOGIN.check(true); + } + if (StringUtils.isBlank(token) && isIgnoreAuth) { + return; + } + try { + session = ssoTouchSessionRpcClient.touchSession(token); + deviceCheck(session, exchange); + log.info("extractedSession session:{}", session); + } catch (Exception e) { + log.error("===> touchSession error : ", e); + //直接抛出未登录异常 + BizResultCode.NO_LOGIN.check(true); + } + + ServerHttpRequest req = exchange.getRequest(); + ServerHttpRequest.Builder requestBuilder = req.mutate(); + // 先删除,后新增 + requestBuilder.headers(k -> k.remove(Constant.GATE_WAY_SESSION_JSON)); + requestBuilder.header(Constant.GATE_WAY_SESSION_JSON, JSON.toJSONString(session)); + ServerHttpRequest request = requestBuilder.build(); + exchange.mutate().request(request).build(); + } + + /** + * 设备号的验证 + * + * @param session + * @param exchange + */ + private void deviceCheck(Session session, ServerWebExchange exchange) { +// //登录时绑定的设备号的验证 +// String reqDeviceId = exchange.getRequest().getHeaders().getFirst(Constant.AUTH_DID); +// BizResultCode.NO_LOGIN.check(StringUtils.isEmpty(reqDeviceId)); +// BizResultCode.NO_LOGIN.check(!reqDeviceId.equals(session.getDeviceCode())); + } + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/filters/IpLimitGlobalFilter.java b/sonic-gateway/src/main/java/com/games/gateway/filters/IpLimitGlobalFilter.java new file mode 100644 index 0000000..95042b3 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/filters/IpLimitGlobalFilter.java @@ -0,0 +1,83 @@ +package com.games.gateway.filters; + +import com.games.gateway.enums.BizResultCode; +import com.games.gateway.config.FilterOrderedConfig; +import com.games.gateway.config.GatewayBizConfig; +import com.games.gateway.enums.LimiterTypeEnum; +import com.games.gateway.utils.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import static org.springframework.http.HttpStatus.BAD_GATEWAY; + +/** + * IP 限流器 + * https://github.com/oneone1995/blog/issues/13 + */ +@Order(FilterOrderedConfig.IpLimitGlobalFilter) +@Slf4j +@Component +public class IpLimitGlobalFilter implements GlobalFilter { + + @Autowired + private LimitUtils limitUtils; + + @Autowired + private GatewayBizConfig globalConfig; + + @Autowired + private LimitCache limitCache; + + @Autowired + private RedisKeyUtils redisKeyUtils; + + @Autowired + private IpBlackCache ipBlackCache; + + /** + * 限制缓存过期时间,秒 + */ + private static Integer EXPIRE_TIME = 60; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + String ip = IpAddressUtils.getIpAddress(request); + //如果是ip黑名单,则不执行 + if (ipBlackCache.isBlack(ip)) { + log.info("ip is black:{}", ip); + exchange.getResponse().setStatusCode(BAD_GATEWAY); + return exchange.getResponse().setComplete(); + } + ip = StringUtils.isNotEmpty(ip) ? ip.replace(":", "-") : ip; + + //application全局设置速率 + Integer ipLimiterNum = globalConfig.getIpLimiter(); + + //mock接口单独对ip设置了速率 + Integer ipLimiter = limitCache.get(LimiterTypeEnum.IP, ip); + if (ipLimiter > 0) { + ipLimiterNum = ipLimiter; + } + //检测是否超过最大访问次数 + String limitKey = redisKeyUtils.ipLimitCheckKey(ip); + boolean b = limitUtils.limitCheck(limitKey, ipLimiterNum, EXPIRE_TIME); + if (b) { + log.info("ip:{} limited", ip); + //设置locale + MessageUtils.setLocale(exchange); + BizResultCode.SYS_OPERATOR_FAST_ERROR.check(true); + return exchange.getResponse().setComplete(); + } + return chain.filter(exchange); + } + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/filters/IpPathLimitGlobalFilter.java b/sonic-gateway/src/main/java/com/games/gateway/filters/IpPathLimitGlobalFilter.java new file mode 100644 index 0000000..478d132 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/filters/IpPathLimitGlobalFilter.java @@ -0,0 +1,67 @@ +package com.games.gateway.filters; + +import com.games.gateway.enums.BizResultCode; +import com.games.gateway.config.FilterOrderedConfig; +import com.games.gateway.domain.IpPathLimit; +import com.games.gateway.utils.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * 路径 限流器 + */ +@Order(FilterOrderedConfig.IpPathLimitGlobalFilter) +@Slf4j +@Component +public class IpPathLimitGlobalFilter implements GlobalFilter { + + @Autowired + private LimitUtils limitUtils; + + @Autowired + private RedisKeyUtils redisKeyUtils; + + @Autowired + private IpPathLimitCache ipPathLimitCache; + + @Autowired + private PathWhiteCache pathWhiteCache; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + String ip = IpAddressUtils.getIpAddress(request); + ip = StringUtils.isNotEmpty(ip) ? ip.replace(":", "-") : ip; + + String path = request.getPath().value(); + //白名单存在path,直接通过 + if (pathWhiteCache.exists(path)) { + return chain.filter(exchange); + } + + String key = redisKeyUtils.ipPathLimitCheckKey(ip, path); + //手动调动接口配置过最大访问次数,过期时间,从缓存存 + IpPathLimit ipPathLimit = ipPathLimitCache.get(path); + if (ipPathLimit != null && ipPathLimit.getCount() != null && ipPathLimit.getExpireTime() != null) { + Integer count = ipPathLimit.getCount(); + Integer expireTime = ipPathLimit.getExpireTime(); + //检测是否超过最大访问次数 + boolean b = limitUtils.limitCheck(key, count, expireTime); + if (b) { + //设置locale + MessageUtils.setLocale(exchange); + BizResultCode.SYS_OPERATOR_FAST_ERROR.check(true); + return exchange.getResponse().setComplete(); + } + } + return chain.filter(exchange); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/filters/LoggingFilter.java b/sonic-gateway/src/main/java/com/games/gateway/filters/LoggingFilter.java new file mode 100644 index 0000000..619ded0 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/filters/LoggingFilter.java @@ -0,0 +1,161 @@ +package com.games.gateway.filters; + +import com.games.gateway.enums.BizResultCode; +import com.games.gateway.utils.IpAddressUtils; +import com.games.gateway.utils.TraceUtils; +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Publisher; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpResponseDecorator; +import org.springframework.util.MultiValueMap; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.charset.Charset; + +/** + * 日志过滤器-打印请求参数返回值及统计执行时长 + * + * @author: zhenqiang.zhan + * @create: 2019-09-11 + */ +@Configuration +@Slf4j +public class LoggingFilter implements GlobalFilter, Ordered { + + /** + * 应用application.name + */ + private static final String APPLICATION_NAME = "application_name"; + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + @Value("${spring.profiles.active}") + private String runMode; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + TraceUtils.setTraceId(request); + String path = request.getPath().value(); + String ip = IpAddressUtils.getIpAddress(request); + String query = request.getURI().getRawQuery(); + String token = exchange.getRequest().getHeaders().getFirst("_tk_"); + //先把请求的日志打印出来 + log.info("==> log path:{} query:{} , ip:{} , token:{}", path, query, ip, token); + + ServerHttpResponse originalResponse = exchange.getResponse(); + AccessRecord accessRecord = new AccessRecord(); + accessRecord.setHost(request.getURI().getHost()); + accessRecord.setPath(request.getURI().getPath()); + accessRecord.setQueryString(request.getQueryParams()); + accessRecord.setHeaders(request.getHeaders()); + + //2022-11-24 加一个环境判断,等android重新发布后直接将这行代码废弃掉 + String platform = exchange.getRequest().getHeaders().getFirst("platform"); + BizResultCode.APP_VERSION_ERROR.check("test".equals(runMode) && "ANDROID_5.3.1".equals(platform)); + + + log.debug(accessRecord.toString()); + Long startTime = System.currentTimeMillis(); + + StringBuffer resultBody = new StringBuffer(); + DataBufferFactory bufferFactory = originalResponse.bufferFactory(); + ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) { + @Override + public Mono writeWith(Publisher body) { + accessRecord.setHttpCode(getStatusCode().value()); + if (getStatusCode().equals(HttpStatus.OK) && body instanceof Flux) { + Flux fluxBody = Flux.from(body); + return super.writeWith(fluxBody.map(dataBuffer -> { + byte[] content = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(content); + //释放掉内存 + DataBufferUtils.release(dataBuffer); + //responseData就是下游系统返回的内容,可以查看修改 + resultBody.append(new String(content, Charset.forName("UTF-8"))); + return bufferFactory.wrap(content); + })); + } else { + log.error("响应code异常:{}", getStatusCode()); + } + return super.writeWith(body); + } + }; + + //从path路径中获取applicationName,然后设置application + setApplicationNameHeader(exchange); + + // replace response with decorator + return chain.filter(exchange.mutate().response(decoratedResponse).build()).doFinally(signal -> { + accessRecord.setBody(resultBody.toString()); + if (startTime != null) { + Long expendTime = (System.currentTimeMillis() - startTime); + accessRecord.setExpendTime(expendTime); + } + log.info(accessRecord.toString()); + }); + } + + /** + * 访问记录对象 + */ + @Data + @ToString + private class AccessRecord { + private String host; + private String path; + private String body; + private MultiValueMap queryString; + private long expendTime; + private int httpCode; + private HttpHeaders headers; + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(" --> path:[").append(path).append("]"); + sb.append(", --> expendTime:[").append(expendTime).append("ms").append("]"); + sb.append(", --> host:[").append(host).append("]"); + sb.append(", --> httpCode:[").append(httpCode).append("]"); + sb.append(", --> queryString:").append(queryString); + sb.append(", --> headers:").append(headers); + sb.append(", --> body:").append(body); + return sb.toString(); + } + } + + /** + * 从path路径中获取applicationName,然后设置application + * + * @param exchange + */ + private void setApplicationNameHeader(ServerWebExchange exchange) { + ServerHttpRequest req = exchange.getRequest(); + String path = req.getPath().value(); + String[] split = path.split("/"); + String applicationName = split[1]; + ServerHttpRequest.Builder requestBuilder = req.mutate(); + requestBuilder.headers(k -> k.remove(APPLICATION_NAME)); + requestBuilder.header(APPLICATION_NAME, applicationName); + ServerHttpRequest request = requestBuilder.build(); + exchange.mutate().request(request).build(); + } +} \ No newline at end of file diff --git a/sonic-gateway/src/main/java/com/games/gateway/filters/PathLimitGlobalFilter.java b/sonic-gateway/src/main/java/com/games/gateway/filters/PathLimitGlobalFilter.java new file mode 100644 index 0000000..c78fde6 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/filters/PathLimitGlobalFilter.java @@ -0,0 +1,75 @@ +package com.games.gateway.filters; + +import com.games.gateway.enums.BizResultCode; +import com.games.gateway.config.FilterOrderedConfig; +import com.games.gateway.config.GatewayBizConfig; +import com.games.gateway.enums.LimiterTypeEnum; +import com.games.gateway.utils.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * 路径 限流器 + */ +@Order(FilterOrderedConfig.PathLimitGlobalFilter) +@Slf4j +@Component +public class PathLimitGlobalFilter implements GlobalFilter { + + @Autowired + private GatewayBizConfig globalConfig; + + @Autowired + private LimitUtils limitUtils; + + @Autowired + private PathWhiteCache pathWhiteCache; + + @Autowired + private LimitCache limitCache; + + @Autowired + private RedisKeyUtils redisKeyUtils; + + + /** + * 限制缓存过期时间,秒 + */ + private static Integer EXPIRE_TIME = 60; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + String path = exchange.getRequest().getPath().value(); + + //白名单存在path,直接通过 + if (pathWhiteCache.exists(path)) { + return chain.filter(exchange); + } + + //application全局设置速率 + Integer pathLimiterNum = globalConfig.getPathLimiter(); + + //mock接口单独对path设置了速率 + Integer pathLimiter = limitCache.get(LimiterTypeEnum.PATH, path); + if (pathLimiter > 0) { + pathLimiterNum = pathLimiter; + } + //检测是否超过最大访问次数 + String limitKey = redisKeyUtils.pathLimitCheckKey(path); + boolean b = limitUtils.limitCheck(limitKey, pathLimiterNum, EXPIRE_TIME); + if (b) { + log.info("path:{} limited", path); + //设置locale + MessageUtils.setLocale(exchange); + BizResultCode.SYS_OPERATOR_FAST_ERROR.check(true); + return exchange.getResponse().setComplete(); + } + return chain.filter(exchange); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/filters/PrintRequestFilter.java b/sonic-gateway/src/main/java/com/games/gateway/filters/PrintRequestFilter.java new file mode 100644 index 0000000..90344af --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/filters/PrintRequestFilter.java @@ -0,0 +1,40 @@ +package com.games.gateway.filters; + +import com.games.gateway.config.FilterOrderedConfig; +import com.games.gateway.utils.IpAddressUtils; +import com.games.gateway.utils.TraceUtils; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * 打印请求日志 不输出 paylod + */ +@Order(value = FilterOrderedConfig.PrintRequestFilter) +@Slf4j +@Component +public class PrintRequestFilter implements GlobalFilter { + + public static List pathIgnore = Lists.newArrayList("/","/robots.txt"); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + TraceUtils.setTraceId(request); + String path = request.getPath().value(); + if(pathIgnore.contains(path)){ + exchange.getResponse().setStatusCode(HttpStatus.OK); + return exchange.getResponse().setComplete(); + } + return chain.filter(exchange); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/filters/ResponseHeaderModifyFilter.java b/sonic-gateway/src/main/java/com/games/gateway/filters/ResponseHeaderModifyFilter.java new file mode 100644 index 0000000..5a2765f --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/filters/ResponseHeaderModifyFilter.java @@ -0,0 +1,26 @@ +package com.games.gateway.filters; + +import com.games.gateway.config.FilterOrderedConfig; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * 用于 remove多的 Access-Control-Allow-Origin + * 兼容底层里面 加了多的 Access-Control-Allow-Origin + */ +@Order(FilterOrderedConfig.ResponseHeaderModifyFilter) +@Component +public class ResponseHeaderModifyFilter implements GlobalFilter { + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + exchange.getResponse().getHeaders().remove("Access-Control-Allow-Origin"); + exchange.getResponse().getHeaders().add("Access-Control-Allow-Origin", "*"); + })); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/filters/TokenLimitGlobalFilter.java b/sonic-gateway/src/main/java/com/games/gateway/filters/TokenLimitGlobalFilter.java new file mode 100644 index 0000000..77f41a3 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/filters/TokenLimitGlobalFilter.java @@ -0,0 +1,77 @@ +package com.games.gateway.filters; + +import com.games.gateway.enums.BizResultCode; +import com.games.gateway.config.FilterOrderedConfig; +import com.games.gateway.config.GatewayBizConfig; +import com.games.gateway.enums.LimiterTypeEnum; +import com.games.gateway.utils.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.List; + + +/** + * token 限速 过滤器 + */ +@Order(FilterOrderedConfig.TokenLimitGlobalFilter) +@Slf4j +@Component +public class TokenLimitGlobalFilter implements GlobalFilter { + + @Autowired + private GatewayBizConfig globalConfig; + + @Autowired + private LimitUtils limitUtils; + + @Autowired + private RedisKeyUtils redisKeyUtils; + + @Autowired + private LimitCache limitCache; + + /** + * 限制缓存过期时间,秒 + */ + private static Integer EXPIRE_TIME = 60; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + List tokenList = exchange.getRequest().getHeaders().get(Constant.AUTH_TK); + if (CollectionUtils.isEmpty(tokenList)) { + return chain.filter(exchange); + } + + String token = tokenList.get(0); + + //application全局设置速率 + Integer tokenLimiterNum = globalConfig.getTokenLimiter(); + + //mock接口单独对ip设置了速率 + Integer tokenLimiter = limitCache.get(LimiterTypeEnum.TOKEN, token); + if (tokenLimiter > 0) { + tokenLimiterNum = tokenLimiter; + } + + //检测是否超过最大访问次数 + String limitKey = redisKeyUtils.tokenLimitCheckKey(token); + boolean b = limitUtils.limitCheck(limitKey, tokenLimiterNum, EXPIRE_TIME); + if (b) { + log.info("token:{} limited", token); + //设置locale + MessageUtils.setLocale(exchange); + BizResultCode.SYS_OPERATOR_FAST_ERROR.check(true); + return exchange.getResponse().setComplete(); + } + return chain.filter(exchange); + } + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/filters/URLBlackListGlobalFilter.java b/sonic-gateway/src/main/java/com/games/gateway/filters/URLBlackListGlobalFilter.java new file mode 100644 index 0000000..d506147 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/filters/URLBlackListGlobalFilter.java @@ -0,0 +1,75 @@ +package com.games.gateway.filters; + +import com.games.gateway.config.FilterOrderedConfig; +import com.games.gateway.utils.AuthCache; +import com.games.gateway.utils.Constant; +import com.games.gateway.utils.MatchStartUrlCheckUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.CollectionUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * 黑名单 全局过滤器 + */ +@Order(FilterOrderedConfig.URLBlackListGlobalFilter) +@Component +@Slf4j +public class URLBlackListGlobalFilter implements GlobalFilter { + + @Autowired + private AuthCache authCache; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + String path = request.getPath().value(); + //获取服务名称 + String applicationName = exchange.getRequest().getHeaders().getFirst(Constant.APPLICATION_NAME); + //验证请求url的前缀是否在配置列表中,如果不再的话则直接拦截掉(用来阻止掉我们不知道的一些开放url被访问到) + boolean notMatchStartUrlBl = MatchStartUrlCheckUtils.notMatchPrefixUrlCheck(applicationName, path); + //检查当前的url是否在请求黑名单中 + boolean inBlackList = inBlackList(path, applicationName); + + log.info("===> URLBlackListGlobalFilter url : {}, applicationName : {}, notMatchStartUrlBl : {}, inBlackList ; {}", path, applicationName, notMatchStartUrlBl, inBlackList); + + if (notMatchStartUrlBl || inBlackList) { + log.info("===> path : {} in block access", path); + exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND); + return exchange.getResponse().setComplete(); + } + return chain.filter(exchange); + } + + /** + * 判断是否在黑名单 内 + * + * @param path + * @return + */ + private boolean inBlackList(String path, String applicationName) { + List urlBlacklist = authCache.getUrlBlacklist(applicationName); + if (!CollectionUtils.isEmpty(urlBlacklist)) { + AntPathMatcher antPathMatcher = new AntPathMatcher(); + for (String uri : urlBlacklist) { + boolean match = antPathMatcher.match(uri, path); + if (match) { + return true; + } + } + } + return false; + } + +} + diff --git a/sonic-gateway/src/main/java/com/games/gateway/rpc/AuthRegisterApi.java b/sonic-gateway/src/main/java/com/games/gateway/rpc/AuthRegisterApi.java new file mode 100644 index 0000000..591f588 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/rpc/AuthRegisterApi.java @@ -0,0 +1,56 @@ +package com.games.gateway.rpc; + +import com.alibaba.fastjson.JSON; +import com.games.gateway.domain.ServiceAuthRegist; +import com.games.gateway.domain.ServiceAuthRegisterRequest; +import com.games.gateway.utils.AesEncodeUtil; +import com.games.gateway.utils.AuthCache; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@Slf4j +@RestController +public class AuthRegisterApi { + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private AuthCache authCache; + + private static final String SECRET_KEY = "l5lUstfWcb3GlWqiiHJKVLKTSvwJSqrT"; + + + @RequestMapping("/") + public Mono root() { + return Mono.just("success"); + } + + + @RequestMapping("/api/gateway/auth/register") + public Mono authRegister(@RequestBody ServiceAuthRegisterRequest registerRequest) { + try { + String payload = AesEncodeUtil.decrypt(registerRequest.getContent(), SECRET_KEY); + log.info("刷新 授权信息:{}", payload); + + ServiceAuthRegist serviceAuthRegist = JSON.parseObject(payload, ServiceAuthRegist.class); + + //覆盖 + String redisKey = "gateway:auth:" + serviceAuthRegist.getApplicationName(); + log.info("设置redis{}", JSON.toJSONString(serviceAuthRegist)); + redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(serviceAuthRegist)); + authCache.cache(redisKey, serviceAuthRegist); + + } catch (Exception e) { + e.printStackTrace(); + log.error("出现上报异常",e); + } + return Mono.just("success"); + } + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/AesEncodeUtil.java b/sonic-gateway/src/main/java/com/games/gateway/utils/AesEncodeUtil.java new file mode 100644 index 0000000..4f0b746 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/AesEncodeUtil.java @@ -0,0 +1,64 @@ +package com.games.gateway.utils; + +import org.apache.commons.codec.binary.Base64; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.validation.constraints.NotNull; +import java.security.Security; +import java.security.spec.AlgorithmParameterSpec; + +public final class AesEncodeUtil { + + //偏移量 + public static final String VIPARA = "dFd8s4fDfV6d2fsD"; + + //编码方式 + private static final String CHARSET_NAME = "UTF-8"; + + private static final String AES_NAME = "AES"; + //填充类型 + public static final String ALGORITHM = "AES/CBC/PKCS7Padding"; + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + public static String genKey(String key) { + return Md5.encode(key).toUpperCase(); + } + + /** + * 加密 + * + * @param content + * @param key + * @return + */ + public static String encrypt(@NotNull String content, @NotNull String key) throws Exception { + byte[] result = null; + Cipher cipher = Cipher.getInstance(ALGORITHM); + SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(CHARSET_NAME), AES_NAME); + AlgorithmParameterSpec paramSpec = new IvParameterSpec(VIPARA.getBytes()); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, paramSpec); + result = cipher.doFinal(content.getBytes(CHARSET_NAME)); + return Base64.encodeBase64String(result); + } + + /** + * 解密 + * + * @param content + * @param key + * @return + */ + public static String decrypt(@NotNull String content, @NotNull String key) throws Exception { + Cipher cipher = Cipher.getInstance(ALGORITHM); + SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(CHARSET_NAME), AES_NAME); + AlgorithmParameterSpec paramSpec = new IvParameterSpec(VIPARA.getBytes()); + cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec); + return new String(cipher.doFinal(Base64.decodeBase64(content)), CHARSET_NAME); + } +} \ No newline at end of file diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/AuthCache.java b/sonic-gateway/src/main/java/com/games/gateway/utils/AuthCache.java new file mode 100644 index 0000000..a7260d4 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/AuthCache.java @@ -0,0 +1,89 @@ +package com.games.gateway.utils; + +import com.alibaba.fastjson.JSON; +import com.games.gateway.config.GatewayBizConfig; +import com.games.gateway.domain.ServiceAuthRegist; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Component +@Slf4j +public class AuthCache implements InitializingBean { + + @Autowired + private RedisTemplate redisTemplate; + + private LoadingCache authCache; + + public ServiceAuthRegist get(String key) { + ServiceAuthRegist serviceAuthRegist = authCache.get(key); + return serviceAuthRegist; + } + + + /** + * 获取指定服务的无需授权登录的URL 列表 + * + * @return + */ + public List getIgnoreAuthPathList(String key) { + List allIgnoreList = new ArrayList<>(50); + ServiceAuthRegist optionalServiceAuthRegist = get(key); + if (optionalServiceAuthRegist != null) { + allIgnoreList.addAll(optionalServiceAuthRegist.getIgnoreAuthList()); + } + return allIgnoreList; + } + + + /** + * 获取指定服务的的黑名单 + * + * @return + */ + public List getUrlBlacklist(String key) { + List urlBlackList = new ArrayList<>(50); + ServiceAuthRegist optionalServiceAuthRegist = get(key); + if (optionalServiceAuthRegist != null) { + urlBlackList.addAll(optionalServiceAuthRegist.getUrlBlacklist()); + } + return urlBlackList; + } + + public void cache(String key, ServiceAuthRegist authRegist) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(key)); + Preconditions.checkNotNull(authRegist); + authCache.put(key, authRegist); + } + + + /** + * 初始化 JVM Cache + */ + @Override + public void afterPropertiesSet() { + authCache = Caffeine.newBuilder() + .expireAfterWrite(30, TimeUnit.SECONDS) + .maximumSize(100) + .build(key -> { + String rediskey = "gateway:auth:" + key; + if (!redisTemplate.hasKey(rediskey)) { + return null; + } + return JSON.parseObject(redisTemplate.opsForValue().get(rediskey), ServiceAuthRegist.class); + }); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/Constant.java b/sonic-gateway/src/main/java/com/games/gateway/utils/Constant.java new file mode 100644 index 0000000..423b072 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/Constant.java @@ -0,0 +1,26 @@ +package com.games.gateway.utils; + +public class Constant { + + /** + * 透传 session对象名称 + */ + public static final String GATE_WAY_SESSION_JSON = "gateway-session-json"; + + /** + * 应用application.name + */ + public static final String APPLICATION_NAME = "application_name"; + + /** + * 授权的设备号 + */ + public static final String AUTH_DID = "AUTH_DID"; + + /** + * 授权的 token + */ + public static final String AUTH_TK = "AUTH_TK"; + + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/IpAddressUtils.java b/sonic-gateway/src/main/java/com/games/gateway/utils/IpAddressUtils.java new file mode 100644 index 0000000..47a3a17 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/IpAddressUtils.java @@ -0,0 +1,60 @@ + +package com.games.gateway.utils; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.server.reactive.ServerHttpRequest; + +public class IpAddressUtils { + private static final Logger log = LoggerFactory.getLogger(IpAddressUtils.class); + + public IpAddressUtils() { + } + + public static String getIpAddress(ServerHttpRequest request) { + String cfIp = request.getHeaders().getFirst("Cf-Connecting-IP"); + if (StringUtils.isNotEmpty(cfIp)) { + return cfIp; + } else { + String awsIp = request.getHeaders().getFirst("x-original-forwarded-for"); + if (StringUtils.isNotEmpty(awsIp)) { + return awsIp; + } else { + String Xip = request.getHeaders().getFirst("X-Real-IP"); + String ip = request.getHeaders().getFirst("X-Forwarded-For"); + if (StringUtils.isNotBlank(ip) && !"unKnown".equalsIgnoreCase(ip)) { + int index = ip.indexOf(","); + return index != -1 ? ip.substring(0, index) : ip; + } else { + ip = Xip; + if (StringUtils.isNotBlank(Xip) && !"unKnown".equalsIgnoreCase(Xip)) { + return Xip; + } else { + if (StringUtils.isBlank(Xip) || "unknown".equalsIgnoreCase(Xip)) { + ip = request.getHeaders().getFirst("Proxy-Client-IP"); + } + + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeaders().getFirst("WL-Proxy-Client-IP"); + } + + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeaders().getFirst("HTTP_CLIENT_IP"); + } + + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeaders().getFirst("HTTP_X_FORWARDED_FOR"); + } + + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddress().toString(); + } + + return ip; + } + } + } + } + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/IpBlackCache.java b/sonic-gateway/src/main/java/com/games/gateway/utils/IpBlackCache.java new file mode 100644 index 0000000..d9724db --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/IpBlackCache.java @@ -0,0 +1,102 @@ +package com.games.gateway.utils; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Component +@Slf4j +public class IpBlackCache implements InitializingBean { + + @Autowired + private RedisTemplate redisTemplate; + + private LoadingCache> ipBlackLocalCache; + + @Autowired + private RedisKeyUtils redisKeyUtils; + + /** + * 判断ip是否是黑名单 + * + * @param ip + * @return + */ + public Boolean isBlack(String ip) { + String ipBlackKey = redisKeyUtils.ipBlackKey(); + List ipBlackList = ipBlackLocalCache.get(ipBlackKey); + return ipBlackList.contains(ip); + } + + /** + * ip黑名单缓存 + * + * @param ipList + */ + public void cache(List ipList) { + String ipBlackKey = redisKeyUtils.ipBlackKey(); + for (String ip : ipList) { + redisTemplate.opsForSet().add(ipBlackKey, ip); + } + List ipBlackList = getIpBlackList(ipBlackKey); + ipBlackLocalCache.put(ipBlackKey, ipBlackList); + } + + /** + * 删除ip黑名单 + * + * @param ipList + */ + public void invalidate(List ipList) { + String tokenCsIpBlackKey = redisKeyUtils.ipBlackKey(); + //删除redis member + redisTemplate.opsForSet().remove(tokenCsIpBlackKey, ipList.toArray()); + + //删除pathList,再重新弄到本地缓存时头 + List ipBlackList = getIpBlackList(tokenCsIpBlackKey); + ipBlackLocalCache.put(tokenCsIpBlackKey, ipBlackList); + } + + + /** + * 获取ip黑名单列表 + * + * @param key + * @return + */ + public List getIpBlackList(String key) { + Set members = redisTemplate.opsForSet().members(key); + if (CollectionUtils.isEmpty(members)) { + return Lists.newArrayList(); + } else { + return members.stream().collect(Collectors.toList()); + } + } + + /** + * 初始化 JVM Cache + */ + @Override + public void afterPropertiesSet() { + ipBlackLocalCache = Caffeine.newBuilder() + .expireAfterWrite(5, TimeUnit.MINUTES) + .maximumSize(100) + .build(key -> { + if (!redisTemplate.hasKey(key)) { + return Lists.newArrayList(); + } + return getIpBlackList(key); + }); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/IpPathLimitCache.java b/sonic-gateway/src/main/java/com/games/gateway/utils/IpPathLimitCache.java new file mode 100644 index 0000000..bb00579 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/IpPathLimitCache.java @@ -0,0 +1,97 @@ +package com.games.gateway.utils; + +import com.alibaba.fastjson.JSON; +import com.games.gateway.domain.IpPathLimit; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.taobao.api.internal.util.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +/** + * @author mzc + */ +@Service +public class IpPathLimitCache implements InitializingBean { + /** + * 默认local session缓存时间 1 分钟 + */ + private static volatile Long DEFAULT_LOCAL_SESSION_CACHE_SECONDS = 1 * 60L; + + private LoadingCache ipPathLimitCache; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Autowired + private RedisKeyUtils redisKeyUtils; + + /** + * 本地缓存使用的兜底对象 + */ + IpPathLimit defaultIpPathLimit = new IpPathLimit(); + + /** + * 获取缓存 + * + * @param path + * @return + */ + public IpPathLimit get(String path) { + // 通过get获取缓存,如果没有,则异步执行load从redis加载数据。即,多线程情况,可能会加载到旧值 + return ipPathLimitCache.get(path); + } + + /** + * 缓存 + * + * @param path + * @param ipPathLimit + */ + public void cache(String path, IpPathLimit ipPathLimit) { + String key = redisKeyUtils.ipPathLimitSetKey(path); + Preconditions.checkArgument(!Strings.isNullOrEmpty(key)); + Preconditions.checkNotNull(key); + //缓存到redis + stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(ipPathLimit)); + //本地缓存 + ipPathLimitCache.put(path, ipPathLimit); + } + + /** + * 失效,删除缓存 + * + * @param path + */ + public void invalidate(String path) { + String key = redisKeyUtils.ipPathLimitSetKey(path); + //删除redis + stringRedisTemplate.delete(key); + //删除本地缓存 + ipPathLimitCache.invalidate(key); + } + + @Override + public void afterPropertiesSet() { + ipPathLimitCache = Caffeine.newBuilder() + .expireAfterWrite(DEFAULT_LOCAL_SESSION_CACHE_SECONDS, TimeUnit.SECONDS) + // TODO: 目前活跃用户不到1w,这里最大按50倍计算,设置50w缓存数量 + .maximumSize(5_000) + .initialCapacity(5_000 / 10) + .recordStats() + .build(path -> { + String key = redisKeyUtils.ipPathLimitSetKey(path); + String cacheJson = stringRedisTemplate.opsForValue().get(key); + if (StringUtils.isEmpty(cacheJson)) { + return defaultIpPathLimit; + } + return JSON.parseObject(cacheJson, IpPathLimit.class); + }); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/LimitCache.java b/sonic-gateway/src/main/java/com/games/gateway/utils/LimitCache.java new file mode 100644 index 0000000..06d6e39 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/LimitCache.java @@ -0,0 +1,66 @@ +package com.games.gateway.utils; + +import com.games.gateway.enums.LimiterTypeEnum; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +@Slf4j +public class LimitCache implements InitializingBean { + + @Autowired + private RedisTemplate redisTemplate; + + private LoadingCache limitCache; + + @Autowired + private RedisKeyUtils redisKeyUtils; + + public String getRedisKey(LimiterTypeEnum type, String key) { + String redisKey = ""; + if (LimiterTypeEnum.TOKEN.equals(type)) { + redisKey = redisKeyUtils.tokenLimiterKey(key); + } else if (LimiterTypeEnum.IP.equals(type)) { + redisKey = redisKeyUtils.ipLimiterKey(key); + } else if (LimiterTypeEnum.PATH.equals(type)) { + redisKey = redisKeyUtils.pathLimiterKey(key); + } + return redisKey; + } + + public Integer get(LimiterTypeEnum type, String key) { + String redisKey = getRedisKey(type, key); + return limitCache.get(redisKey); + } + + public void cache(LimiterTypeEnum type, String key, Integer num) { + String redisKey = getRedisKey(type, key); + redisTemplate.opsForValue().set(redisKey, String.valueOf(num)); + limitCache.put(redisKey, Integer.valueOf(num)); + } + + /** + * 初始化 JVM Cache + */ + @Override + public void afterPropertiesSet() { + limitCache = Caffeine.newBuilder() + .expireAfterWrite(30, TimeUnit.SECONDS) + .maximumSize(500_000) + .initialCapacity(500_000 / 50) + .build(key -> { + if (!redisTemplate.hasKey(key)) { + return 0; + } + String numStr = redisTemplate.opsForValue().get(key); + return Integer.valueOf(numStr); + }); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/LimitUtils.java b/sonic-gateway/src/main/java/com/games/gateway/utils/LimitUtils.java new file mode 100644 index 0000000..0f1ff92 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/LimitUtils.java @@ -0,0 +1,78 @@ +package com.games.gateway.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * @description: ip+path限制工具 + * @author: mzc + * @date: 2022-09-16 15:44 + **/ +@Slf4j +@Component +public class LimitUtils { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * redis key不存在的过期时间值 + */ + private Integer NULL_KEY_EXP_TIME_VALUE = -2; + + /** + * redis key永不过期的过期时间值 + */ + private Integer NEVER_EXPIRES_VALUE = -1; + + + public boolean limitCheck(String redisKey, int count, int time) { + //用户操作某个话题的点赞,超过4次就提示,锁定5s + int num; + Long expTime = stringRedisTemplate.getExpire(redisKey); + if (!stringRedisTemplate.hasKey(redisKey)) { + num = 1; + stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", time, TimeUnit.SECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(redisKey, 1).intValue(); + } + + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if (expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis并返回false + stringRedisTemplate.delete(redisKey); + return false; + } + + if (num >= count) { + log.info("===>超过了限定的次数[{}],{}", count, redisKey); + return true; + } + return false; + } + + + public Integer getCacheNum(String redisKey, int time) { + //用户操作某个话题的点赞,超过4次就提示,锁定5s + int num; + Long expTime = stringRedisTemplate.getExpire(redisKey); + if (!stringRedisTemplate.hasKey(redisKey)) { + num = 1; + stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", time, TimeUnit.SECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(redisKey, 1).intValue(); + } + + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if (expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis并返回false + stringRedisTemplate.delete(redisKey); + return 0; + } + return num; + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/MatchStartUrlCheckUtils.java b/sonic-gateway/src/main/java/com/games/gateway/utils/MatchStartUrlCheckUtils.java new file mode 100644 index 0000000..22e3953 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/MatchStartUrlCheckUtils.java @@ -0,0 +1,63 @@ +package com.games.gateway.utils; + +import com.google.common.collect.Lists; +import org.springframework.util.CollectionUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @Author zzhan + * @Description 授权可访问的起始url校验 + * @Date 2023/10/27 15:11 + * @Version 1.0 + */ +public class MatchStartUrlCheckUtils { + + /** + * 配置各个服务可访问url路径前缀 + */ + private static final Map> START_URL_CONFIG_MAP = new HashMap<>(); + + static { + + //配置frog服务可访问url的路径 + START_URL_CONFIG_MAP.put("frog", Lists.newArrayList("/mobile/", "/web/", "/mock/", "/probe/")); + + //配置cow服务可访问url的路径 + START_URL_CONFIG_MAP.put("cow", Lists.newArrayList("/mobile/", "/web/", "/mock/", "/probe/")); + + //配置pigeon服务可访问url的路径 + START_URL_CONFIG_MAP.put("pigeon", Lists.newArrayList("/mobile/", "/web/", "/mock/", "/probe/", "/open-api/")); + + //配置shark服务可访问url的路径 + START_URL_CONFIG_MAP.put("shark", Lists.newArrayList("/mobile/", "/web/", "/mock/", "/probe/")); + + //配置lion服务可访问url的路径 + START_URL_CONFIG_MAP.put("lion", Lists.newArrayList("/mobile/", "/web/", "/mock/", "/probe/")); + + } + + + /** + * 校验不匹配访问url前缀的接口请求 + * @param applicationName + * @param url + * @return + */ + public static boolean notMatchPrefixUrlCheck(String applicationName, String url) { + List prefixList = START_URL_CONFIG_MAP.get(applicationName); + if(CollectionUtils.isEmpty(prefixList)) { + return false; + } + for (String prefix : prefixList) { + if(url.startsWith(prefix)) { + return false; + } + } + return true; + } + + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/Md5.java b/sonic-gateway/src/main/java/com/games/gateway/utils/Md5.java new file mode 100644 index 0000000..4a9d30b --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/Md5.java @@ -0,0 +1,37 @@ +package com.games.gateway.utils; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class Md5 { + + private Md5() { + } + + public static String encode(String str) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(str.getBytes()); + byte[] byteDigest = md.digest(); + StringBuffer buf = new StringBuffer(""); + + for (int offset = 0; offset < byteDigest.length; ++offset) { + int i = byteDigest[offset]; + if (i < 0) { + i += 256; + } + + if (i < 16) { + buf.append("0"); + } + + buf.append(Integer.toHexString(i)); + } + + return buf.toString(); + } catch (NoSuchAlgorithmException var6) { + var6.printStackTrace(); + return ""; + } + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/MessageUtils.java b/sonic-gateway/src/main/java/com/games/gateway/utils/MessageUtils.java new file mode 100644 index 0000000..f668454 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/MessageUtils.java @@ -0,0 +1,76 @@ +package com.games.gateway.utils; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; + +import java.util.Locale; + +/** + * 国际化工具类 + * + * @author zzhan + * @description 国际化工具 + * @date 2019/5/27 16:43 + */ +@Component +public class MessageUtils { + + private static MessageSource messageSource; + + + @Autowired + public void setMessageSource(MessageSource messageSource) { + MessageUtils.messageSource = messageSource; + } + + /** + * 获取单个国际化翻译值 + */ + public static String get(String msgKey) { + try { + String message = messageSource.getMessage(msgKey, null, LocaleContextHolder.getLocale()); + return message; + } catch (Exception e) { + e.printStackTrace(); + return msgKey; + } + } + + public static String get(String msgKey, Locale locale) { + try { + String message = messageSource.getMessage(msgKey, null, locale); + return message; + } catch (Exception e) { + return msgKey; + } + } + + public static String getMessage(String code, Locale locale) { + try { + String message = messageSource.getMessage(code, null, locale); + return message; + } catch (Exception e) { + return code; + } + } + + public static String getMessage(String code, Object[] args, Locale locale) { + return messageSource.getMessage(code, args, locale); + } + + /** + * 设置locale + * + * @param exchange + */ + public static void setLocale(ServerWebExchange exchange) { + String lang = exchange.getRequest().getHeaders().getFirst("accept-language"); + lang = lang == null ? exchange.getRequest().getHeaders().getFirst("Accept-Language") : lang; + if (lang != null) { + LocaleContextHolder.setLocale(new Locale(lang)); + } + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/PathWhiteCache.java b/sonic-gateway/src/main/java/com/games/gateway/utils/PathWhiteCache.java new file mode 100644 index 0000000..299c7c7 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/PathWhiteCache.java @@ -0,0 +1,111 @@ +package com.games.gateway.utils; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.collect.Lists; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author mzc + */ +@Service +public class PathWhiteCache implements InitializingBean { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Autowired + private RedisKeyUtils redisKeyUtils; + + private LoadingCache> pathWhiteLocalCache; + + public String getRedisKey() { + String redisKey = redisKeyUtils.pathWhiteKey(); + return redisKey; + } + + /** + * 检测path是否是白名单 + * + * @param path + * @return + */ + public Boolean exists(String path) { + String key = getRedisKey(); + List pathWhiteList = pathWhiteLocalCache.get(key); + return pathWhiteList.contains(path); + } + + /** + * 缓存 + * + * @param pathList + */ + public void cache(List pathList) { + String key = getRedisKey(); + //先添加到redis + for (String path : pathList) { + stringRedisTemplate.opsForSet().add(key, path); + } + //再添加本地缓存 + List pathWhiteList = getPathWhiteList(key); + pathWhiteLocalCache.put(key, pathWhiteList); + + } + + /** + * 删除缓存 + * + * @param path + */ + public void invalidate(String path) { + String key = redisKeyUtils.pathWhiteKey(); + //删除redis member + stringRedisTemplate.opsForSet().remove(path); + + //再删除本地缓存 + List pathWhiteList = getPathWhiteList(key); + pathWhiteLocalCache.put(key, pathWhiteList); + } + + /** + * 获取白名单列表 + * + * @param key + * @return + */ + public List getPathWhiteList(String key) { + Set members = stringRedisTemplate.opsForSet().members(key); + if (CollectionUtils.isEmpty(members)) { + return Lists.newArrayList(); + } else { + List pathWhiteList = members.stream().collect(Collectors.toList()); + return pathWhiteList; + } + } + + /** + * 初始化 JVM Cache + */ + @Override + public void afterPropertiesSet() { + pathWhiteLocalCache = Caffeine.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .maximumSize(100) + .build(key -> { + if (!stringRedisTemplate.hasKey(key)) { + return Lists.newArrayList(); + } + return getPathWhiteList(key); + }); + } +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/RedisKeyUtils.java b/sonic-gateway/src/main/java/com/games/gateway/utils/RedisKeyUtils.java new file mode 100644 index 0000000..d54be1c --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/RedisKeyUtils.java @@ -0,0 +1,152 @@ +package com.games.gateway.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 统一的 redis key 管理工具类(方便后续的维护和查找) + * + * @Author zzhan + * @Date 2021/9/24 + * @Version 1.0 + */ +@Slf4j +@Service +public class RedisKeyUtils { + + /** + * ip+path限流计数检查key + * + * @param ip + * @param path + * @return + */ + public String ipPathLimitCheckKey(String ip, String path) { + return "gateway:ip:path:limit:check:".concat(path).concat(":").concat(ip); + } + + /** + * ip+path缓存配置key + * + * @param path + * @return + */ + public String ipPathLimitSetKey(String path) { + return "gateway:ip:path:limit:set:".concat(path); + } + + + /** + * path白名单key + * + * @return + */ + public String pathWhiteKey() { + return "gateway:path:white"; + } + + /** + * ip 每分钟最大访问次数限制 + * + * @return + */ + public String ipLimiterKey(String ip) { + return "gateway:ipLimiter:" + ip; + } + + /** + * path 每分钟最大访问次数限制 + * + * @return + */ + public String pathLimiterKey(String path) { + return "gateway:pathLimiter:" + path; + } + + /** + * 频繁token撞库对应IP限制次数限制 1小时 + * + * @return + */ + public String tokenCsIpLimiterKey() { + return "gateway:tokenCsIpLimiter"; + } + + /** + * path限流计数检查key + * + * @param path + * @return + */ + public String pathLimitCheckKey(String path) { + return "gateway:limiter:path:check:" + path; + } + + /** + * ip限流计算检查key + * + * @param ip + * @return + */ + public String ipLimitCheckKey(String ip) { + return "gateway:limiter:ip:check:" + ip; + } + + /** + * 频繁token撞库对应IP限制计算检查 1小时 + * + * @param ip + * @return + */ + public String tokenCsIpLimitCheckKey(String ip) { + return "gateway:limiter:tokenCsIp:check:" + ip; + } + + /** + * 频繁token撞库对应IP黑名单 set + * + * @return + */ + public String ipBlackKey() { + return "gateway:ipBlack:"; + } + + /** + * token限流计算检查key + * + * @param token + * @return + */ + public String tokenLimitCheckKey(String token) { + return "gateway:limiter:token:check:" + token; + } + + /** + * token每分钟最大访问次数限制 + * + * @param token + * @return + */ + public String tokenLimiterKey(String token) { + return "gateway:tokenLimiter:" + token; + } + + /** + * token撞库的统计次数 + * + * @return + */ + public String tokenCsLimiterKey() { + return "gateway:token:csCount"; + } + + /** + * token撞库的ip set + * + * @return + */ + public String tokenCsIpKey() { + return "gateway:token:cs:ip"; + } + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/RedisUtils.java b/sonic-gateway/src/main/java/com/games/gateway/utils/RedisUtils.java new file mode 100644 index 0000000..4c0d9f6 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/RedisUtils.java @@ -0,0 +1,54 @@ +package com.games.gateway.utils; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +/** + * @description: redis 工具类 + * @author: mzc + * @date: 2022-09-21 16:23 + **/ +@Component +public class RedisUtils { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * redis string设置 + * + * @param key + * @param val + */ + public void stringSet(String key, String val) { + stringRedisTemplate.opsForValue().set(key, val); + } + + /** + * redis string获取 + * + * @param key + * @return + */ + public String stringGet(String key) { + return stringRedisTemplate.opsForValue().get(key); + } + + /** + * redis string获取 + * + * @param key + * @return + */ + public Integer stringGetToInt(String key) { + String str = stringRedisTemplate.opsForValue().get(key); + if (StringUtils.isNotEmpty(str)) { + return Integer.valueOf(str); + } else { + return 0; + } + } + +} diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/SpringContextUtil.java b/sonic-gateway/src/main/java/com/games/gateway/utils/SpringContextUtil.java new file mode 100644 index 0000000..506b0c3 --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/SpringContextUtil.java @@ -0,0 +1,42 @@ +package com.games.gateway.utils; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.util.Locale; + +@Component +public class SpringContextUtil implements ApplicationContextAware { + + private static ApplicationContext context = null; + + /* (non Javadoc) + * @Title: setApplicationContext + * @Description: spring获取bean工具类 + * @param applicationContext + * @throws BeansException + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.context = applicationContext; + } + + // 传入线程中 + public static T getBean(String beanName) { + return (T) context.getBean(beanName); + } + + // 国际化使用 + public static String getMessage(String key) { + return context.getMessage(key, null, Locale.getDefault()); + } + + /// 获取当前环境 + public static String getActiveProfile() { + return context.getEnvironment().getActiveProfiles()[0]; + } +} \ No newline at end of file diff --git a/sonic-gateway/src/main/java/com/games/gateway/utils/TraceUtils.java b/sonic-gateway/src/main/java/com/games/gateway/utils/TraceUtils.java new file mode 100644 index 0000000..af316cb --- /dev/null +++ b/sonic-gateway/src/main/java/com/games/gateway/utils/TraceUtils.java @@ -0,0 +1,32 @@ +package com.games.gateway.utils; + +import org.slf4j.MDC; +import org.springframework.http.server.reactive.ServerHttpRequest; + +import java.util.UUID; + +public class TraceUtils { + + private static final String TRACE_ID = "TRACE_ID"; + private static final String HTTP_HEADER_TRACE_ID = "X-TRACE-ID"; + + public TraceUtils() { + } + public static void setTraceId( ServerHttpRequest request ) { + //之在 第一个过滤器加 TRACE_ID 就 省去了 返回结果之前的清理逻辑。 + String traceId = UUID.randomUUID().toString(); + MDC.put(TRACE_ID, traceId); + request.mutate().header(HTTP_HEADER_TRACE_ID, traceId); + } + public static String getTraceId() { + return MDC.get(TRACE_ID); + } + + public static void removeTraceId() { + MDC.remove(TRACE_ID); + } + + public static void setTraceId(String internalTraceId) { + MDC.put(TRACE_ID, internalTraceId); + } +} diff --git a/sonic-gateway/src/main/resources/application-dev.yml b/sonic-gateway/src/main/resources/application-dev.yml new file mode 100644 index 0000000..bb51159 --- /dev/null +++ b/sonic-gateway/src/main/resources/application-dev.yml @@ -0,0 +1,65 @@ +spring: + redis: + database: 0 + host: 54.223.196.180 + port: 6379 + password: 123456 + + cloud: + gateway: + httpclient: + response-timeout: 120000 + default-filters: + - StripPrefix=1 + routes: + - id: "bear-service" + uri: "http://test-bear-svc:8080" + predicates: + - Path=/bear/** + - id: "frog-service" + uri: "http://test-frog-svc:8080" + predicates: + - Path=/frog/** + - id: "lion-service" + uri: "http://test-lion-svc:8080" + predicates: + - Path=/lion/** + - id: "shark-service" + uri: "http://test-shark-svc:8080" + predicates: + - Path=/shark/** + - id: "cow-service" + uri: "http://test-cow-svc:8080" + predicates: + - Path=/cow/** + - id: "shark-pigeon" + uri: "http://test-pigeon-svc:8080" + predicates: + - Path=/pigeon/** + +gateway: + ipPath24HoursLimiter: 4000 + ipLimiter: 500 + tokenCsIpLimiter: 100 + pathLimiter: 4000 + tokenLimiter: 2000 + bearHost: http://test-bear-svc:8080 + applicationNames: + - bear + - frog + - lion + - shark + - cow + +redisson: + nettyThreads: 8 + address: redis://${spring.redis.host}:${spring.redis.port} + database: 0 + password: ${spring.redis.password} + timeout: 3000 + connection-pool-size: 20 + connection-minimum-idle-size: 20 + +# +server: + port: 8082 \ No newline at end of file diff --git a/sonic-gateway/src/main/resources/application-local.yml b/sonic-gateway/src/main/resources/application-local.yml new file mode 100644 index 0000000..9c6890f --- /dev/null +++ b/sonic-gateway/src/main/resources/application-local.yml @@ -0,0 +1,65 @@ +spring: + redis: + database: 0 + host: 192.168.100.238 + port: 6379 + password: Epal@2020 + + cloud: + gateway: + httpclient: + response-timeout: 120000 + default-filters: + - StripPrefix=1 + routes: + - id: "bear-service" + uri: "http://test-bear-svc:8080" + predicates: + - Path=/bear/** + - id: "frog-service" + uri: "http://test-frog-svc:8080" + predicates: + - Path=/frog/** + - id: "lion-service" + uri: "http://test-lion-svc:8080" + predicates: + - Path=/lion/** + - id: "shark-service" + uri: "http://test-shark-svc:8080" + predicates: + - Path=/shark/** + - id: "cow-service" + uri: "http://test-cow-svc:8080" + predicates: + - Path=/cow/** + - id: "shark-pigeon" + uri: "http://test-pigeon-svc:8080" + predicates: + - Path=/pigeon/** + +gateway: + ipPath24HoursLimiter: 4000 + ipLimiter: 500 + tokenCsIpLimiter: 100 + pathLimiter: 4000 + tokenLimiter: 2000 + bearHost: http://test-bear-svc:8080 + applicationNames: + - bear + - frog + - lion + - shark + - cow + +redisson: + nettyThreads: 8 + address: redis://${spring.redis.host}:${spring.redis.port} + database: 0 + password: ${spring.redis.password} + timeout: 3000 + connection-pool-size: 20 + connection-minimum-idle-size: 20 + +# +server: + port: 8082 \ No newline at end of file diff --git a/sonic-gateway/src/main/resources/application-product.yml b/sonic-gateway/src/main/resources/application-product.yml new file mode 100644 index 0000000..0ccc8d5 --- /dev/null +++ b/sonic-gateway/src/main/resources/application-product.yml @@ -0,0 +1,65 @@ +spring: + redis: +# database: 0 +# host: ${REDIS.GATEWAY.HOST} +# port: ${REDIS.GATEWAY.PORT} +# password: ${REDIS.GATEWAY.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + cloud: + gateway: + httpclient: + response-timeout: 120000 + default-filters: + - StripPrefix=1 + routes: + - id: "bear-service" + uri: "http://test-bear-svc:8080" + predicates: + - Path=/bear/** + - id: "frog-service" + uri: "http://test-frog-svc:8080" + predicates: + - Path=/frog/** + - id: "lion-service" + uri: "http://test-lion-svc:8080" + predicates: + - Path=/lion/** + - id: "shark-service" + uri: "http://test-shark-svc:8080" + predicates: + - Path=/shark/** + - id: "cow-service" + uri: "http://test-cow-svc:8080" + predicates: + - Path=/cow/** + metadata: + response-timeout: 120000 + - id: "shark-pigeon" + uri: "http://test-pigeon-svc:8080" + predicates: + - Path=/pigeon/** + +gateway: + ipPath24HoursLimiter: 4000 + ipLimiter: 500 + tokenCsIpLimiter: 100 + pathLimiter: 4000 + tokenLimiter: 2000 + bearHost: http://test-bear-svc:8080 + applicationNames: + - bear + - frog + - lion + - shark + - cow + +redisson: + nettyThreads: 8 + address: redis://${spring.redis.host}:${spring.redis.port} + database: 0 + password: ${spring.redis.password} + timeout: 3000 + connection-pool-size: 20 + connection-minimum-idle-size: 20 + diff --git a/sonic-gateway/src/main/resources/application-test.yml b/sonic-gateway/src/main/resources/application-test.yml new file mode 100644 index 0000000..3bfa088 --- /dev/null +++ b/sonic-gateway/src/main/resources/application-test.yml @@ -0,0 +1,65 @@ +spring: + redis: +# database: 0 +# host: ${REDIS.GATEWAY.HOST} +# port: ${REDIS.GATEWAY.PORT} +# password: ${REDIS.GATEWAY.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + + cloud: + gateway: + httpclient: + response-timeout: 120000 + default-filters: + - StripPrefix=1 + routes: + - id: "bear-service" + uri: "http://test-bear-svc:8080" + predicates: + - Path=/bear/** + - id: "frog-service" + uri: "http://test-frog-svc:8080" + predicates: + - Path=/frog/** + - id: "lion-service" + uri: "http://test-lion-svc:8080" + predicates: + - Path=/lion/** + - id: "shark-service" + uri: "http://test-shark-svc:8080" + predicates: + - Path=/shark/** + - id: "cow-service" + uri: "http://test-cow-svc:8080" + predicates: + - Path=/cow/** + metadata: + response-timeout: 120000 + - id: "shark-pigeon" + uri: "http://test-pigeon-svc:8080" + predicates: + - Path=/pigeon/** +gateway: + ipPath24HoursLimiter: 4000 + ipLimiter: 500 + tokenCsIpLimiter: 100 + pathLimiter: 4000 + tokenLimiter: 2000 + bearHost: http://test-bear-svc:8080 + applicationNames: + - bear + - frog + - lion + - shark + - cow + +redisson: + nettyThreads: 8 + address: redis://${spring.redis.host}:${spring.redis.port} + database: 0 + password: ${spring.redis.password} + timeout: 3000 + connection-pool-size: 20 + connection-minimum-idle-size: 20 + diff --git a/sonic-gateway/src/main/resources/application.yml b/sonic-gateway/src/main/resources/application.yml new file mode 100644 index 0000000..7e8e8c3 --- /dev/null +++ b/sonic-gateway/src/main/resources/application.yml @@ -0,0 +1,21 @@ +spring: + profiles: + active: local + application: + name: gateway + cloud: + gateway: + httpclient: + connect-timeout: 45000 + response-timeout: 20s + +language: + # 默认语言类型 + default: en + # 所有的语言类型(新增语言后需要在此进行配置) + available: en + +# 禁用健康检查,有漏洞,任何人不的擅自打开【后果自负】 +management: + endpoints: + enabled-by-default: false #关闭监控 \ No newline at end of file diff --git a/sonic-gateway/src/main/resources/logback-spring.xml b/sonic-gateway/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..e89354a --- /dev/null +++ b/sonic-gateway/src/main/resources/logback-spring.xml @@ -0,0 +1,56 @@ + + + + + + + + + ${DefaultPattern} + + + + + + + + ${ColorfulPattern} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sonic-gateway/src/main/resources/messages.properties b/sonic-gateway/src/main/resources/messages.properties new file mode 100644 index 0000000..cd81f52 --- /dev/null +++ b/sonic-gateway/src/main/resources/messages.properties @@ -0,0 +1,3 @@ +NO_LOGIN=User Not Signed In +SYS_OPERATOR_FAST_ERROR=Operation is too frequent, try again later +APP_VERSION_ERROR=AppVersion error diff --git a/sonic-gateway/src/main/resources/messages_en.properties b/sonic-gateway/src/main/resources/messages_en.properties new file mode 100644 index 0000000..50e6037 --- /dev/null +++ b/sonic-gateway/src/main/resources/messages_en.properties @@ -0,0 +1,3 @@ +NO_LOGIN=User Not Signed In +SYS_OPERATOR_FAST_ERROR=Operation is too frequent, try again later +APP_VERSION_ERROR=AppVersion error \ No newline at end of file diff --git a/sonic-lion/.gitignore b/sonic-lion/.gitignore new file mode 100644 index 0000000..51bb6a0 --- /dev/null +++ b/sonic-lion/.gitignore @@ -0,0 +1,26 @@ +/**/rebel.xml + +#target/ +target/ + +# IDEA # +.idea/ +*.iml + +# Eclipse # +.settings/ +.classpath +.project + +# Log # +logs/ +*.log% + +#mac +.DS_Store + +#svn +.svn +#reviewboardrc +.reviewboardrc +**/rest-client.private.env.json diff --git a/sonic-lion/bootstrap-guide.md b/sonic-lion/bootstrap-guide.md new file mode 100644 index 0000000..ba12dc6 --- /dev/null +++ b/sonic-lion/bootstrap-guide.md @@ -0,0 +1,41 @@ +# 项目定制化手册 +## 定制化步骤 +* 确定项目依赖的组件比如redis,mysql,rabbitmq, 然后搜索`TODO`把不需要的依赖和多余的目录移除. +* 确定项目依赖组件后, 请在application-${env}中配置对应的资源地址. +* 运行单元测试, 保证单元测试全部通过. + +## 模块介绍 +### common +在该模块添加其他模块共用的lib,例如common-lib以及常用的guava,fastjson等
+主要是考虑到项目可能有多个部署的模块,通过将共用的lib定义在common模块中,可以简化其他模块的配置 + +### server +可部署的后端服务,包含SpringBoot的入口以及该服务相关的client,config,entity,dao, service,controller等 + +#### config +定义配置信息和错误code + +#### client +定义访问依赖的第三方服务的客户端接口. 访问依赖方服务,必须通过Client接口封装,禁止业务代码调用http相关逻辑. + +#### entity +定义领域对象. + +#### service +主要定义业务逻辑代码 + +#### controller +对外暴露的API定义 + +#### test +单元测试模块. 为了保证交付的质量和服务的演进,核心逻辑需要编写单元测试, + +##### 目录文件 +- java + - ClientStubs 第三方依赖客户端的Stub实现. + - BaseTest 单元测试基类. 建议每个单元测试从它基础 +- resources + - mysql 存放数据库的schema和测试数据. schema文件可以作为schema变化的版本记录, 同时也是H2数据库初始化脚本. + +### integration-test +集成测试,测试已部署服务的APIs diff --git a/sonic-lion/common/pom.xml b/sonic-lion/common/pom.xml new file mode 100644 index 0000000..e3f7a46 --- /dev/null +++ b/sonic-lion/common/pom.xml @@ -0,0 +1,58 @@ + + + + sonic-lion + com.sonic.lion + 1.0 + + 4.0.0 + + sonic-lion-common + jar + 1.0 + + + + com.sonic + common-lib + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + mysql + mysql-connector-java + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.google.guava + guava + + + com.alibaba + fastjson + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-pool2 + + + org.springframework.boot + spring-boot-starter-data-redis + + + diff --git a/sonic-lion/common/src/main/java/com/sonic/lion/common/GlobalConfig.java b/sonic-lion/common/src/main/java/com/sonic/lion/common/GlobalConfig.java new file mode 100644 index 0000000..2297521 --- /dev/null +++ b/sonic-lion/common/src/main/java/com/sonic/lion/common/GlobalConfig.java @@ -0,0 +1,115 @@ +package com.sonic.lion.common; + +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.AppRuntime; +import com.sonic.common.client.JobmanClient; +import com.sonic.common.client.impl.JobmanClientImpl; +import com.sonic.common.log.RequestLogAspect; +import com.sonic.common.rpc.HttpClient; +import com.sonic.common.rpc.RpcClient; +import com.sonic.common.rpc.RpcClientImpl; +import com.sonic.common.stat.FrequencyAlertInterceptor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.task.TaskExecutorBuilder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.task.TaskExecutor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.stream.Collectors; + +import static org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME; + +/** + * @author code + */ +@Slf4j +public class GlobalConfig { + @Value("${spring.application.name}") + private String appName; + @Value("${spring.application.id}") + private String appId; + + @Bean + ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() { + return new ScheduledThreadPoolExecutor(1); + } + + /** + * XXX Spring默认配置当有Executor的bean后不再装载TaskExecutor, 这里因为手动注册了 + * {@link #scheduledThreadPoolExecutor}会导致spring不再自动注册TaskExecutor, 因此需要去掉ConditionalOnMissingBean的限制. + * 参考{@link TaskExecutionAutoConfiguration#applicationTaskExecutor} + */ + @Bean(name = {APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME}) + public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { + return builder.build(); + } + + @Bean + public RequestLogAspect requestLogAspect() { + return new RequestLogAspect(); + } + + @Bean + AppRuntime appRuntime(ApplicationContext applicationContext) { + return AppRuntime.builder().appId(appId).appName(appName).applicationContext(applicationContext).build(); + } + + /** + * 用于配置接口访问频率超限告警, 接口频率配置参考application.yml中的web.frequency-alert.rules + * TODO 根据需要配置为输出到日志或者alertClient + * + * @return + */ + @Bean + public FrequencyAlertInterceptor frequencyAlertInterceptor(RuleConfig ruleConfig) { + return new FrequencyAlertInterceptor(ruleConfig.getAlertRuleMap(), + (request, frequencyAlert) -> log.warn("frequency alert, api frequency alert, url = {} alert = {}", + request.getRequestURI(), JSONObject.toJSONString(frequencyAlert))); + } + + @Bean + @Primary + public RpcClient rpcClient() { + return new RpcClientImpl(HttpClient.Config.EMPTY); + } + + @Bean + public JobmanClient jobmanClient(AppRuntime appRuntime, TaskExecutor taskExecutor, RedisTemplate redisTemplate) { + return new JobmanClientImpl.Builder().appRuntime(appRuntime).taskExecutor(taskExecutor).redisTemplate(redisTemplate).build(); + } + + @Data + @Component + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "web.frequency-alert") + public static class RuleConfig { + private List rules; + + public Map getAlertRuleMap() { + if (rules == null) { + return Collections.emptyMap(); + } + return rules.stream() + .map(e -> { + String[] split = e.split(":"); + return new AbstractMap.SimpleEntry<>(split[0], split[1]); + }).collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); + } + } +} diff --git a/sonic-lion/common/src/main/java/com/sonic/lion/common/MybatisPlusConfig.java b/sonic-lion/common/src/main/java/com/sonic/lion/common/MybatisPlusConfig.java new file mode 100644 index 0000000..ed81d95 --- /dev/null +++ b/sonic-lion/common/src/main/java/com/sonic/lion/common/MybatisPlusConfig.java @@ -0,0 +1,19 @@ +package com.sonic.lion.common; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; + +@EnableTransactionManagement +@MapperScan("com.sonic.**.dao") +public class MybatisPlusConfig { + + @Bean + public PaginationInterceptor paginationInterceptor() { + PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); + paginationInterceptor.setDialectType("mysql"); + return paginationInterceptor; + } +} diff --git a/sonic-lion/lib/pom.xml b/sonic-lion/lib/pom.xml new file mode 100644 index 0000000..4618a8d --- /dev/null +++ b/sonic-lion/lib/pom.xml @@ -0,0 +1,43 @@ + + + + sonic-lion + com.sonic.lion + 1.0 + + 4.0.0 + + com.sonic.lion + sonic-lion-lib + jar + 1.0-SNAPSHOT + + + + + com.sonic + common-lib + + + + log4j-api + org.apache.logging.log4j + + + com.alibaba + fastjson + + + + + + com.alibaba + fastjson + 1.2.83 + + + + + \ No newline at end of file diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/client/PayClient.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/client/PayClient.java new file mode 100644 index 0000000..b663f6a --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/client/PayClient.java @@ -0,0 +1,111 @@ +package com.sonic.lion.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.lion.lib.input.BalanceCheckoutInput; +import com.sonic.lion.lib.input.GetAccountBuffInput; +import com.sonic.lion.lib.input.PlatformGiftInput; +import com.sonic.lion.lib.output.AccountBuffOutput; +import com.sonic.lion.lib.output.BalanceCheckoutOutput; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author code + */ +@Slf4j +@Service +public class PayClient { + + private static final String CREATE_ACCOUNT_BUFF = "/api/pay/trade/create-account-buff"; + private static final String GET_ACCOUNT_BUFF = "/api/pay/trade/get-account-buff"; + private static final String CHECKOUT_TO_USER = "/api/pay/trade/checkoutToUser"; + private static final String CHECKOUT_TO_B = "/api/pay/trade/checkoutToB"; + private static final String PLATFORM_GIFT = "/api/pay/trade/systemGift"; + + private RpcClient rpcClient; + private String host; + + public PayClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-lion-svc:8080"; + break; + case product: + default: + this.host = "http://prod-lion-svc:8080"; + } + } + + /** + * 创建账号钱包 + * + * @param userId + * @return + */ + public void createAccountBuff(Long userId) { + GetAccountBuffInput input = new GetAccountBuffInput(userId); + rpcClient.post(host + CREATE_ACCOUNT_BUFF, input, new TypeReference>() { + }); + } + + /** + * 获取账号钱包数据 + * + * @param userId + * @return + */ + public AccountBuffOutput getAccountBuff(Long userId) { + GetAccountBuffInput input = new GetAccountBuffInput(userId); + return rpcClient.post(host + GET_ACCOUNT_BUFF, input, new TypeReference>() { + }); + } + + /** + * 钱包余额结账给用户 + * + * @param input + * @return + */ + public BalanceCheckoutOutput checkoutToUser(BalanceCheckoutInput input) { + return rpcClient.post(host + CHECKOUT_TO_USER, input, new TypeReference>() { + }); + } + + /** + * 钱包余额结账给平台 + * + * @param input + * @return + */ + public BalanceCheckoutOutput checkoutToB(BalanceCheckoutInput input) { + return rpcClient.post(host + CHECKOUT_TO_B, input, new TypeReference>() { + }); + } + + /** + * 平台赠送 + * + * @param input + * @return + */ + public Long platformGift(PlatformGiftInput input) { + return rpcClient.post(host + PLATFORM_GIFT, input, new TypeReference>() { + }); + } + + public void saveUserBill(BalanceCheckoutInput input) { + } + + public void checkoutToPlatformNotBill(BalanceCheckoutInput input) { + } +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/client/SubscribeClient.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/client/SubscribeClient.java new file mode 100644 index 0000000..1e75fef --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/client/SubscribeClient.java @@ -0,0 +1,65 @@ +package com.sonic.lion.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.Lists; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.lion.lib.input.GetSubscribeInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author code + */ +@Slf4j +@Service +public class SubscribeClient { + + private static final String SUBSCRIBE_QUERY = "/api/pay/subscribe/query"; + + private RpcClient rpcClient; + private String host; + + public SubscribeClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-lion-svc:8080"; + break; + case product: + default: + this.host = "http://prod-lion-svc:8080"; + } + } + + /** + * 获取用户是否是会员 + * @param userId + * @return + */ + public Boolean queryUserIsSubscribe(Long userId) { + List userIdList = queryUserIsSubscribe(Lists.newArrayList(userId)); + return userIdList.contains(userId); + } + + /** + * 批量获取用户是否是会员 + * @param userIdList + * @return + */ + public List queryUserIsSubscribe(List userIdList) { + GetSubscribeInput input = new GetSubscribeInput(); + input.setUserIdList(userIdList); + return rpcClient.post(host + SUBSCRIBE_QUERY, input, new TypeReference>>(){}); + } + +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/AccountTypeEnum.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/AccountTypeEnum.java new file mode 100644 index 0000000..10cb1c5 --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/AccountTypeEnum.java @@ -0,0 +1,36 @@ +package com.sonic.lion.lib.enums; + +import lombok.Getter; + +/** + * 账号类型 + * @author code + */ +@Getter +public enum AccountTypeEnum { + /** + * 客户用户 + */ + CUSTOMER(1), + /** + * 后台账号 + */ + SYSTEM(2), + ; + + private final Integer code; + + AccountTypeEnum(Integer code) { + this.code = code; + } + + public static AccountTypeEnum from(Integer code) { + for (AccountTypeEnum status : AccountTypeEnum.values()) { + if (code.equals(status.code)) { + return status; + } + } + + return null; + } +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/BizType.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/BizType.java new file mode 100644 index 0000000..82e160a --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/BizType.java @@ -0,0 +1,82 @@ +package com.sonic.lion.lib.enums; + +import lombok.Getter; + +/** + * @author: code + * @date: 2025/05/07 + * @Description: 交易类型 + * @version: 1.0.0 + */ +@Getter +public enum BizType { + + /** + * 创建图片 + */ + CREATE_AI_IMAGE(610, "Create AI Image", TradeType.C2B), + /** + * 文本模型 + */ + TEXT_MODEL(620, "Text Model", TradeType.C2B), + /** + * 聊天辅助 + */ + CHAT_ASSISTANT(630, "Chat Assistant", TradeType.C2B), + /** + * 发送语音 + */ + SEND_VOICE(640, "Send Voice", TradeType.C2B), + /** + * 语音电话 + */ + VOICE_CALL(650, "Voice Call", TradeType.C2B), + /** + * 解锁上锁图片(10%计入官方虚拟账户,90%归用户虚拟钱包) + */ + IMAGE_UNLOCK(700, "Image Unlock", TradeType.NON_SECURED_TRANSACTION), + /** + * 购买心动值 + */ + HEARTBEAT_PURCHASE(800, "Heartbeat Purchase", TradeType.C2B), + /** + * 虚拟礼物(10%计入官方虚拟账户,90%归用户虚拟钱包) + */ + GIFT(900, "Gift", TradeType.NON_SECURED_TRANSACTION), + /** + * 解锁爱慕者 + */ + UNLOCK_ADMIRERS(1000, "Unlock Admirers", TradeType.C2B), + + /** + * 新注册用户赠送 + */ + NEW_USER_GIFT(1001, "New User Gift", TradeType.NON_SECURED_TRANSACTION), + + /** + * VIP用户赠送 + */ + VIP_BUFF_GIFT(1002, "VIP User Gift", TradeType.NON_SECURED_TRANSACTION), + + /** + * 用户签到赠送 + */ + SIGN_IN_GIFT(1003, "Sign In Gift", TradeType.NON_SECURED_TRANSACTION), + + + GIFT_TO_P(1004, "", TradeType.NON_SECURED_TRANSACTION), + IMAGE_UNLOCK_TO_P(1005, "", TradeType.NON_SECURED_TRANSACTION); + + private final int value; + + private final String desc; + + private final TradeType tradeType; + + + BizType(int value, String desc, TradeType tradeType) { + this.value = value; + this.desc = desc; + this.tradeType = tradeType; + } +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/OptTypeEnum.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/OptTypeEnum.java new file mode 100644 index 0000000..d5605ed --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/OptTypeEnum.java @@ -0,0 +1,7 @@ +package com.sonic.lion.lib.enums; + +public enum OptTypeEnum { + ADD, + DEL, + UPD +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/ThirdTypeEnum.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/ThirdTypeEnum.java new file mode 100644 index 0000000..7a20fa8 --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/ThirdTypeEnum.java @@ -0,0 +1,13 @@ +package com.sonic.lion.lib.enums; + +/** + * 三方类型枚举 + */ +public enum ThirdTypeEnum { + + DISCORD, + GOOGLE, + APPLE, + ; + +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/TradeStatus.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/TradeStatus.java new file mode 100644 index 0000000..ff8b29c --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/TradeStatus.java @@ -0,0 +1,43 @@ +package com.sonic.lion.lib.enums; + +/** + * @author: code + * @date: 2025/05/07 + * @Description: 支付状态 + * @version: 1.0.0 + */ +public enum TradeStatus { + + WAITPAY(1, "待付款"), + + PAID(2, "已付款"), + + PROCESSING(3, "处理中"), + + FINISHED(4, "交易成功"), + + CLOSED(5, "交易关闭"), + + REFUNDING(6, "退款中"), + + REFUNDED(7, "已退款"), + ; + + private final int value; + + private final String desc; + + TradeStatus(Integer value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/TradeType.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/TradeType.java new file mode 100644 index 0000000..ddfb6ca --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/TradeType.java @@ -0,0 +1,55 @@ +package com.sonic.lion.lib.enums; + +import lombok.Getter; + +/** + * @author: code + * @date: 2025/07/22 + * @Description: 交易类型 + * @version: 1.0.0 + */ +@Getter +public enum TradeType { + + /** + * 担保交易 + * 付款方先将钱付给系统担保账户,付款方确认完成服务后再将钱由系统担保账户转至收款方账户 + */ + SECURED_TRANSACTION(1, "Secured Transaction"), + + /** + * 非担保交易 + * 直接将钱从付款方账户转至收款方账户 + */ + NON_SECURED_TRANSACTION(2, "Non Secured Transaction"), + + /** + * 退款 + */ + REFUND(4, "Refund"), + + /** + * 提现 + */ + WITHDRAW(5, "Withdraw"), + + /** + * 充值 + */ + CHARGE(6, "Charge"), + + /** + * 用户付款给系统 + */ + C2B(7, "C2B"), + ; + + private final int value; + + private final String desc; + + TradeType(int value, String desc) { + this.value = value; + this.desc = desc; + } +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/UserTypeEnum.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/UserTypeEnum.java new file mode 100644 index 0000000..a7910ab --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/enums/UserTypeEnum.java @@ -0,0 +1,17 @@ +package com.sonic.lion.lib.enums; + +import lombok.Getter; + +@Getter +public enum UserTypeEnum { + + USER(1), + AI(2); + + private Integer code; + + UserTypeEnum(Integer code) { + this.code = code; + } + +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/BalanceCheckoutInput.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/BalanceCheckoutInput.java new file mode 100644 index 0000000..7648b44 --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/BalanceCheckoutInput.java @@ -0,0 +1,107 @@ +package com.sonic.lion.lib.input; + +import com.sonic.lion.lib.enums.BizType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BalanceCheckoutInput { + + /** + * 平台代码(Balance 钱包余额) + */ + @NotBlank + private String platform = "Balance"; + + /** + * 平台抽成费用 + */ + private Long platformFee; + + /** + * 外部交易号(如订单号) + */ + @NotBlank + private String outTradeNo; + + /** + * 外部交易号关联单号 + */ + private String outTradeNoRelationNo; + + /** + * 业务类型 + */ + @NotNull + private BizType bizType; + + /** + * 交易标题或名称 + */ + @NotBlank + private String name; + + /** + * 本方(发起方)账户(account id) + */ + @NotNull + private Long srcAccountId; + + /** + * 对方账户 + */ + private Long desAccountId; + + /** + * 商品金额,单位:分 + */ + @Min(1) + @NotNull + private Long productAmount; + + /** + * 优惠金额,单位:分 + */ + @NotNull + private Long promoAmount = 0L; + + /** + * 备注 + */ + private String remark; + + /** + * 交易关闭时间 + */ + private LocalDateTime closeTime; + + /** + * 资源key + */ + private String resourceKey; + + /** + * 资源数量 + */ + private Integer resourceNum; + + /** + * 扩展信息 + */ + private String extend; + + /** + * ip 地址 + */ + private String ip; +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/GetAccountBuffInput.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/GetAccountBuffInput.java new file mode 100644 index 0000000..686bee8 --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/GetAccountBuffInput.java @@ -0,0 +1,18 @@ +package com.sonic.lion.lib.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GetAccountBuffInput { + /** + * 用户Id + */ + private Long uid; +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/GetSubscribeInput.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/GetSubscribeInput.java new file mode 100644 index 0000000..93c968b --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/GetSubscribeInput.java @@ -0,0 +1,21 @@ +package com.sonic.lion.lib.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GetSubscribeInput { + + /** + * 用户ID列表 + */ + private List userIdList; +} diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/PlatformGiftInput.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/PlatformGiftInput.java new file mode 100644 index 0000000..33635a2 --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/input/PlatformGiftInput.java @@ -0,0 +1,69 @@ +package com.sonic.lion.lib.input; + +import com.sonic.lion.lib.enums.BizType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Data +public class PlatformGiftInput { + private String outTradeNo; + private String platform; + private String extend; + private Long uid; + private String name; + private LocalDateTime createTime; + private Long amount; //赠送金额 + private Type type; + + private String ip; + private String reason; + + + @AllArgsConstructor + @Getter + public enum Type { + + NEW_USER_GIFT(BizType.NEW_USER_GIFT, false, BuffType.BALANCE), //新手礼 + VIP_BUFF_GIFT(BizType.VIP_BUFF_GIFT, false, BuffType.BALANCE), //订阅VIP 赠送BUFF + SIGN_IN_GIFT(BizType.SIGN_IN_GIFT, false, BuffType.BALANCE), //签到 赠送BUFF + ; + private BizType bizType; //业务类型 + private boolean needConfirm; //是否需要确认。 + private BuffType buffType; + } + + + @Getter + public enum BuffType { + /** + * 可消费Buff + */ + BALANCE(1, "BALANCE"), + /** + * 可提现收入 + */ + WITHDRAWABLE_INCOME(2, "WITHDRAWABLE_INCOME"), + /** + * 待入账收入 + */ + AWAITING_INCOME(3, "AWAITING_INCOME"), + /** + * 冻结收入 + */ + FROZEN_INCOME(4, "FROZEN_INCOME"), + FROZEN_BALANCE(5, "FROZEN_BALANCE"), + ; + + private final int value; + private final String desc; + + BuffType(int value, String desc) { + this.value = value; + this.desc = desc; + } + } + +} \ No newline at end of file diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/output/AccountBuffOutput.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/output/AccountBuffOutput.java new file mode 100644 index 0000000..47d72e6 --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/output/AccountBuffOutput.java @@ -0,0 +1,85 @@ +package com.sonic.lion.lib.output; + +import lombok.*; + +/** + * 账号余额 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AccountBuffOutput { + + /** + * 用户id + */ + private Long uid; + + /** + * 可消费金额 + */ + private Long balance; + + /** + * 可提现收入 + */ + private Long withdrawableIncome; + + /** + * 待入账收入 + */ + private Long awaitingIncome; + + /** + * 冻结收入 + */ + private Long frozenIncome; + + /** + * 冻结余额 + */ + private Long frozenBalance; + + /** + * 总充值 + */ + private long rechargeTotal; + + /** + * 在途的 提现金额 + */ + private Long withdrawOnGoing; + + /** + * 状态 + */ + private Status status; + + @Getter + public enum Status { + + ENABLE(0, "ENABLE"), + + DISABLE(1, "DISABLE"), + ; + + private final int value; + + private final String desc; + + Status(int value, String desc) { + this.value = value; + this.desc = desc; + } + } + + /** + * 获取总金额 + * + * @return + */ + public long getTotalAmount() { + return this.getBalance() + this.getWithdrawableIncome() + this.getAwaitingIncome() + this.getFrozenIncome(); + } +} \ No newline at end of file diff --git a/sonic-lion/lib/src/main/java/com/sonic/lion/lib/output/BalanceCheckoutOutput.java b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/output/BalanceCheckoutOutput.java new file mode 100644 index 0000000..3e15fa6 --- /dev/null +++ b/sonic-lion/lib/src/main/java/com/sonic/lion/lib/output/BalanceCheckoutOutput.java @@ -0,0 +1,26 @@ +package com.sonic.lion.lib.output; + +import com.sonic.lion.lib.enums.TradeStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/05/20 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BalanceCheckoutOutput { + + private String paymentUrl; + + private TradeStatus tradeStatus; + + private Long fee; +} diff --git a/sonic-lion/pom.xml b/sonic-lion/pom.xml new file mode 100644 index 0000000..236dc25 --- /dev/null +++ b/sonic-lion/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + com.sonic + common-parent-pom + 1.0 + + + sonic-lion + + com.sonic.lion + pom + 1.0 + + + + 1.0.6 + 1.0 + + + + + + + com.sonic + common-lib + ${common-lib.version} + + + com.sonic + dao-support-lib + ${dao-support-lib.version} + + + com.google.cloud + libraries-bom + 20.9.0 + pom + import + + + + + + + org.projectlombok + lombok + + + + + common + server + lib + + + + + + + + + + + + + diff --git a/sonic-lion/server/pom.xml b/sonic-lion/server/pom.xml new file mode 100644 index 0000000..37b2fa5 --- /dev/null +++ b/sonic-lion/server/pom.xml @@ -0,0 +1,223 @@ + + + + sonic-lion + com.sonic.lion + 1.0 + + 4.0.0 + + sonic-lion-server + jar + + + + com.sonic.lion + sonic-lion-common + 1.0 + + + commons-logging + commons-logging + + + animal-sniffer-annotations + org.codehaus.mojo + + + + + + com.sonic.sdk + sonic-common-api + 1.0.1 + + + + com.sonic + dao-support-lib + 1.0 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + it.ozimov + embedded-redis + + + + + + org.springframework.amqp + spring-amqp + + + org.springframework.amqp + spring-rabbit + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + + + com.h2database + h2 + + + + org.redisson + redisson-spring-boot-starter + 3.13.1 + + + + com.paypal.sdk + rest-api-sdk + 1.14.0 + + + com.paypal.sdk + checkout-sdk + 1.0.2 + + + + com.stripe + stripe-java + 20.108.0 + + + + com.google.apis + google-api-services-androidpublisher + v3-rev46-1.25.0 + + + com.google.j2objc + j2objc-annotations + + + google-api-client + com.google.api-client + + + + + com.google.cloud + google-cloud-pubsub + + + com.google.cloud + google-cloud-storage + + + + com.mashape.unirest + unirest-java + 1.4.9 + + + + + + + + + + com.auth0 + jwks-rsa + 0.9.0 + + + io.jsonwebtoken + jjwt-api + 0.11.2 + + + io.jsonwebtoken + jjwt-impl + 0.11.2 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.2 + runtime + + + org.bitbucket.b_c + jose4j + 0.6.4 + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + com.sonic.sdk + sonic-common-api + 1.0.1 + + + + log4j-api + org.apache.logging.log4j + + + com.alibaba + fastjson + + + + + com.sonic.lion + sonic-lion-lib + 1.0-SNAPSHOT + compile + + + com.sonic.pigeon + sonic-pigeon-lib + 1.1-SNAPSHOT + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pl.project13.maven + git-commit-id-plugin + + + false + false + + + + + diff --git a/sonic-lion/server/src/main/java/com/google/api/services/androidpublisher/model/ProductPurchase.java b/sonic-lion/server/src/main/java/com/google/api/services/androidpublisher/model/ProductPurchase.java new file mode 100644 index 0000000..c32e049 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/google/api/services/androidpublisher/model/ProductPurchase.java @@ -0,0 +1,129 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.google.api.services.androidpublisher.model; + +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonString; +import com.google.api.client.util.Key; + +public final class ProductPurchase extends GenericJson { + @Key + private Integer consumptionState; + @Key + private String developerPayload; + @Key + private String kind; + @Key + private String orderId; + @Key + private Integer purchaseState; + @Key + @JsonString + private Long purchaseTimeMillis; + @Key + private Integer purchaseType; + + @Key + private String obfuscatedExternalAccountId; + + @Key + private String obfuscatedExternalProfileId; + + + + public ProductPurchase() { + } + + public Integer getConsumptionState() { + return this.consumptionState; + } + + public ProductPurchase setConsumptionState(Integer var1) { + this.consumptionState = var1; + return this; + } + + public String getDeveloperPayload() { + return this.developerPayload; + } + + public ProductPurchase setDeveloperPayload(String var1) { + this.developerPayload = var1; + return this; + } + + public String getKind() { + return this.kind; + } + + public ProductPurchase setKind(String var1) { + this.kind = var1; + return this; + } + + public String getOrderId() { + return this.orderId; + } + + public ProductPurchase setOrderId(String var1) { + this.orderId = var1; + return this; + } + + public Integer getPurchaseState() { + return this.purchaseState; + } + + public ProductPurchase setPurchaseState(Integer var1) { + this.purchaseState = var1; + return this; + } + + public Long getPurchaseTimeMillis() { + return this.purchaseTimeMillis; + } + + public ProductPurchase setPurchaseTimeMillis(Long var1) { + this.purchaseTimeMillis = var1; + return this; + } + + public Integer getPurchaseType() { + return this.purchaseType; + } + + public ProductPurchase setPurchaseType(Integer var1) { + this.purchaseType = var1; + return this; + } + + + public String getObfuscatedExternalAccountId() { + return obfuscatedExternalAccountId; + } + + public void setObfuscatedExternalAccountId(String obfuscatedExternalAccountId) { + this.obfuscatedExternalAccountId = obfuscatedExternalAccountId; + } + + public String getObfuscatedExternalProfileId() { + return obfuscatedExternalProfileId; + } + + public void setObfuscatedExternalProfileId(String obfuscatedExternalProfileId) { + this.obfuscatedExternalProfileId = obfuscatedExternalProfileId; + } + + @Override + public ProductPurchase set(String var1, Object var2) { + return (ProductPurchase)super.set(var1, var2); + } + + @Override + public ProductPurchase clone() { + return (ProductPurchase)super.clone(); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/MainApplication.java b/sonic-lion/server/src/main/java/com/sonic/lion/MainApplication.java new file mode 100644 index 0000000..ef0646a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/MainApplication.java @@ -0,0 +1,27 @@ +package com.sonic.lion; + +import com.sonic.sdk.api.annotation.EnableGatWayAuthScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.retry.annotation.EnableRetry; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * @author: code + * @date: 2025/05/08 + * @Description: + * @version: 1.0.0 + */ +@EnableSwagger2 +@SpringBootApplication +@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true) +@ComponentScan(value = {"com.sonic"}) +@EnableRetry +@EnableGatWayAuthScan(basePackages = "com.sonic.lion.controller") +public class MainApplication { + public static void main(String[] args) { + SpringApplication.run(MainApplication.class, args); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/channel/ApplicationContextUtil.java b/sonic-lion/server/src/main/java/com/sonic/lion/channel/ApplicationContextUtil.java new file mode 100644 index 0000000..6b68738 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/channel/ApplicationContextUtil.java @@ -0,0 +1,21 @@ +package com.sonic.lion.channel; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Component +public class ApplicationContextUtil implements ApplicationContextAware { + private static ApplicationContext context; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + context = applicationContext; + } + + public static ApplicationContext getContext() { + return context; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/channel/config/StripeConfig.java b/sonic-lion/server/src/main/java/com/sonic/lion/channel/config/StripeConfig.java new file mode 100644 index 0000000..d57d0ee --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/channel/config/StripeConfig.java @@ -0,0 +1,63 @@ +package com.sonic.lion.channel.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import java.math.BigDecimal; + +@Data +@Configuration +public class StripeConfig { + + @Value("${stripe.apiKey}") + private String apiKey; + + @Value("${stripe.webhookSecret}") + private String webhookSecret; + + @Value("${stripe.mode}") + private String mode; + + @Value("${stripe.paymentFeeBase}") + private Long paymentFeeBase; + + @Value("${stripe.paymentFeeRate}") + private BigDecimal paymentFeeRate; + + @Value("${stripe.withdrawFeeBase}") + private Long withdrawFeeBase; + + @Value("${stripe.withdrawFeeRate}") + private Double withdrawFeeRate; + + // Webhook secrets for different event types + @Value("${stripe.webhookSec.payment}") + private String paymentWebhookSecret; + + @Value("${stripe.webhookSec.payout}") + private String payoutWebhookSecret; + + @Value("${stripe.webhookSec.subscription}") + private String subscriptionWebhookSecret; + + @Value("${stripe.webhookSec.dispute}") + private String disputeWebhookSecret; + + // Session URLs + @Value("${stripe.session.successUrl:}") + private String successUrl; + + @Value("${stripe.session.cancelUrl:}") + private String cancelUrl; + + @Value("${stripe.sub.successUrl:}") + private String subSuccessUrl; + + @Value("${stripe.sub.cancelUrl:}") + private String subCancelUrl; + + // Portal URL + @Value("${stripe.portal.returnUrl:}") + private String returnUrl; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/config/Config.java b/sonic-lion/server/src/main/java/com/sonic/lion/config/Config.java new file mode 100644 index 0000000..b961dbb --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/config/Config.java @@ -0,0 +1,48 @@ +package com.sonic.lion.config; + +import com.sonic.common.AppRuntime; +import com.sonic.common.config.DefaultWebMvcConfig; +import com.sonic.common.exception.AbstractExceptionHandler; +import com.sonic.lion.common.GlobalConfig; +import com.sonic.lion.common.MybatisPlusConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; + +import java.util.function.Consumer; + +/** + * Server服务唯一的配置入口 + * TODO: Import的各种Config,按需使用(例如,如果不需要mysql,kafka消息,redis等,可以去掉对应的Config) + * + * @author code + */ +@Slf4j +@Configuration +@Import({GlobalConfig.class, MybatisPlusConfig.class, RedisConfig.class, DefaultWebMvcConfig.class, + SsoConfig.class, RestConfig.class, EventConfig.class}) +public class Config { + + /** + * XXX: ExceptionHandlerConfig 应该在 DefaultWebMvcConfig 前被初始. 否则 DefaultWebMvcConfig 的 exceptionHandlerHook 会生效 + */ + static class ExceptionHandlerConfig { + @Bean + @Profile({"!unittest && !local"}) + AbstractExceptionHandler.ExceptionHandlerHook exceptionHandlerHook(AppRuntime appRuntime) { + return new AbstractExceptionHandler.ExceptionHandlerHook() { + @Override + public String getAppId() { + return appRuntime.getAppId(); + } + + @Override + public Consumer getExceptionConsumer() { + return context -> log.error("未捕获内部异常, logText: {}", context.dumpRequest(), context.getException()); + } + }; + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/config/EventConfig.java b/sonic-lion/server/src/main/java/com/sonic/lion/config/EventConfig.java new file mode 100644 index 0000000..ef8c063 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/config/EventConfig.java @@ -0,0 +1,133 @@ +package com.sonic.lion.config; + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.ImmutableMap; +import com.rabbitmq.client.Channel; +import com.sonic.common.AppRuntime; +import com.sonic.common.event.*; +import com.sonic.common.utils.LogUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.annotation.RabbitListeners; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Payload; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * @author code + */ +@Slf4j +public class EventConfig { + + /** + * TODO: 定义 Event.BuildInScene + */ + public final static String DEFAULT_SCENE = Event.BuildInScene.SONIC.getCode(); + /** + * TODO: DEFAULT_SCENE + "_" + appName + */ + public final static String DEFAULT_MODULE = DEFAULT_SCENE + "_" + "lion"; + + + @Value("${mq.exchange.default-exchange}") + private String defaultExchange; + + @Value("${mq.default.queue}") + private String defaultQueue; + + @Value("${mq.default.routing-key}") + private String defaultRoutingKey; + + @Configuration + @Profile("!unittest") + static class Listener { + @Autowired + EventConsumer eventConsumer; + + @RabbitListeners({ + @RabbitListener(queues = {"${mq.default.queue}"}, concurrency = "1") + }) + public void process(@Payload Message message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) { + try { + LogUtils.setTraceId(); + byte[] msgBody = message.getBody(); + String contentEncoding = message.getMessageProperties().getContentEncoding(); + String bodyStr = new String(msgBody, Charset.forName(ObjectUtils.defaultIfNull(contentEncoding, "UTF-8"))); + MessageProperties pro = message.getMessageProperties(); + if (log.isDebugEnabled()) { + log.info("receive msg routingKey:{}->consumerQueue:{}->redelivered:{}->body:{}", + pro.getReceivedRoutingKey(), pro.getConsumerQueue(), pro.getRedelivered(), bodyStr); + } + eventConsumer.onEvent(bodyStr, ImmutableMap.of("record", JSON.toJSONString(message))); + + //消息确认,multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息 + channel.basicAck(deliveryTag, false); + } catch (Exception e) { + log.error("===> mq handler error, message: {}", JSON.toJSONString(message), e); + //消息拒绝,//requeue=true,表示将消息重新放入到队列中,false:表示直接从队列中删除,此时和basicAck(long deliveryTag, false)的效果一样 + try { + channel.basicReject(deliveryTag, true); + } catch (IOException ioException) { + log.error("channel.basicReject error. message: {}, deliveryTag: {}", JSON.toJSONString(message), deliveryTag, ioException); + } + } finally { + LogUtils.removeTraceId(); + } + } + + } + + @Bean + EventProducer eventProducer(AppRuntime appRuntime, RabbitTemplate rabbitTemplate, TaskExecutor taskExecutor) { + BiConsumer callback = (event, meta) -> { + }; + return new RabbitmqEventProducer(rabbitTemplate, DEFAULT_MODULE, DEFAULT_SCENE, appRuntime.getAppId(), + RabbitmqEventProducer.RabbitmqMessageMeta.build(defaultExchange, defaultRoutingKey), taskExecutor, callback); + } + + @Bean + EventConsumer eventConsumer(AppRuntime appRuntime, EventHandlerRepository eventHandlerRepository) { + Consumer callback = (eventWrapper) -> { + }; + return new DefaultEventConsumer(appRuntime.getAppName(), eventHandlerRepository, callback); + } + + @Bean + EventHandlerRepository eventHandlerRepository() { + return new EventHandlerRepository((ex, logText) -> log.error("MQ,handle error {}", logText, ex)); + } + + @Bean + public DirectExchange defaultExchange() { + return new DirectExchange(defaultExchange); + } + + @Bean + public Binding bindingDefaultQueueExchange(DirectExchange defaultExchange, Queue defaultQueue) { + return bindingExchange(defaultExchange, defaultQueue, defaultRoutingKey); + } + + @Bean + public Queue defaultQueue() { + return new Queue(defaultQueue, true); + } + + public Binding bindingExchange(DirectExchange exchange, Queue queueMessage, String routingKey) { + return BindingBuilder.bind(queueMessage).to(exchange).with(routingKey); + } +} + diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/config/RedisConfig.java b/sonic-lion/server/src/main/java/com/sonic/lion/config/RedisConfig.java new file mode 100644 index 0000000..33dbce1 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/config/RedisConfig.java @@ -0,0 +1,36 @@ +package com.sonic.lion.config; + +import com.sonic.common.utils.RedisLock; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * 有关Redis的配置 + * + * TODO: 如果不需要Redis, 可以删除该文件并且在{@link Config}类@Import的注解中移除对RedisConfig的引用 + */ +@Slf4j +public class RedisConfig { + + /** + * 配置 RedisWrapper 用于分布式锁 + */ + @Bean + RedisLock.RedisWrapper redisWrapper(RedisTemplate redisTemplate) { + return new RedisLock.RedisWrapper() { + @Override + public void delete(String key) { + redisTemplate.delete(key); + } + + @Override + public boolean lock(String key, String value, long expireMills) { + return redisTemplate.opsForValue().setIfAbsent(key, value, expireMills, TimeUnit.MILLISECONDS); + } + }; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/config/RedissonConfig.java b/sonic-lion/server/src/main/java/com/sonic/lion/config/RedissonConfig.java new file mode 100644 index 0000000..42d3538 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/config/RedissonConfig.java @@ -0,0 +1,52 @@ +package com.sonic.lion.config; + +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + + /** + * 配置 RedissonClient,支持单机和集群模式 + */ + @Bean + public RedissonClient redissonClient(RedisProperties redisProperties) { + Config config = new Config(); + + // 设置 Netty 线程数,使用默认值 0(Redisson 默认使用 CPU 核心数的两倍) + config.setNettyThreads(0); + + // 判断是否配置了集群节点 + if (redisProperties.getCluster() != null && redisProperties.getCluster().getNodes() != null + && !redisProperties.getCluster().getNodes().isEmpty()) { + // 集群模式配置 + config.useClusterServers() + .setScanInterval(2000) // 集群状态扫描间隔,单位毫秒 + .addNodeAddress(redisProperties.getCluster().getNodes().stream() + .map(node -> "redis://" + node) + .toArray(String[]::new)) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null); + } else { + // 单机模式配置 + config.useSingleServer() + .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort()) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null) + .setDatabase(redisProperties.getDatabase()); + } + + // 如果启用了 SSL,设置 SSL 相关配置 + if (redisProperties.isSsl()) { + config.useSingleServer().setSslEnableEndpointIdentification(false); // 禁用主机名验证 + // 集群模式下,Redisson 会自动应用 SSL 配置 + } + + return Redisson.create(config); + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/config/RestConfig.java b/sonic-lion/server/src/main/java/com/sonic/lion/config/RestConfig.java new file mode 100644 index 0000000..0acd205 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/config/RestConfig.java @@ -0,0 +1,36 @@ +package com.sonic.lion.config; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +/** + * @author: code + * @date: 2025/06/24 + * @Description: + * @version: 1.0.0 + */ +public class RestConfig implements RestTemplateCustomizer { + + @Override + public void customize(RestTemplate restTemplate) { + if(restTemplate.getRequestFactory() instanceof OkHttp3ClientHttpRequestFactory) { + OkHttp3ClientHttpRequestFactory okHttpClient = (OkHttp3ClientHttpRequestFactory)restTemplate.getRequestFactory(); + okHttpClient.setConnectTimeout(20000); + okHttpClient.setReadTimeout(20000); + } else if(restTemplate.getRequestFactory() instanceof HttpComponentsClientHttpRequestFactory) { + HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate.getRequestFactory(); + factory.setConnectTimeout(20000); + factory.setReadTimeout(20000); + } + + } + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder.build(); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/config/ResultCode.java b/sonic-lion/server/src/main/java/com/sonic/lion/config/ResultCode.java new file mode 100644 index 0000000..623aae5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/config/ResultCode.java @@ -0,0 +1,50 @@ +package com.sonic.lion.config; + +import com.sonic.common.rpc.ApiResultCode; +import lombok.Getter; + +/** + * @author code + */ +@Getter +public enum ResultCode implements ApiResultCode { + + /** + * TODO: 可以在此处扩展服务自身需要用到的错误码信息 + */ + BIZ_ERROR1("000", "业务异常1"), + DEMO_CREATED_FAIL("001", "新增Demo实体失败"); + + /** + * result code + */ + private String errorCode; + + /** + * result message + */ + private String errorMsg; + + ResultCode(String errorCode, String errorMsg) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1004"; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/config/SsoConfig.java b/sonic-lion/server/src/main/java/com/sonic/lion/config/SsoConfig.java new file mode 100644 index 0000000..428bf40 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/config/SsoConfig.java @@ -0,0 +1,13 @@ +package com.sonic.lion.config; + +import com.sonic.common.auth.GateWaySessionInterceptor; +import org.springframework.context.annotation.Bean; + +public class SsoConfig { + + @Bean + public GateWaySessionInterceptor gateWaySessionInterceptor() { + return new GateWaySessionInterceptor(); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/config/SwaggerConfig.java b/sonic-lion/server/src/main/java/com/sonic/lion/config/SwaggerConfig.java new file mode 100644 index 0000000..41af762 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/config/SwaggerConfig.java @@ -0,0 +1,129 @@ +package com.sonic.lion.config; + + +import com.sonic.common.auth.domains.Session; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.RequestHandler; +import springfox.documentation.annotations.ApiIgnore; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; + +/** + * swagger配置 + * @author code + */ +@Slf4j +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + @Value("${swagger.enabled}") + private Boolean swaggerEnabled = false; + + @Value("${swagger.base.package}") + private String basePackageValue; + + private static final String SPLIT = ","; + + private final String DEFAULT_BASE_PACKAGE = "com.sonic.web.controller"; + +// @Bean +// public Docket createRestApi() { +// basePackageValue = null == basePackageValue || ("").equals(basePackageValue) ? DEFAULT_BASE_PACKAGE : basePackageValue; +// log.info("===> swagger base package : ", basePackageValue); +// return new Docket(DocumentationType.SWAGGER_2) +// .apiInfo(apiInfo()) +// .enable(swaggerEnabled) +// .select() +// .apis(basePackage(basePackageValue)) +// .paths(PathSelectors.any()) +// .build(); +// } + + @Bean + public Docket createRestApi() { + basePackageValue = null == basePackageValue || ("").equals(basePackageValue) ? DEFAULT_BASE_PACKAGE : basePackageValue; + log.info("===> swagger base package : ", basePackageValue); + + //在配置好的配置类中增加此段代码即可 + ParameterBuilder ticketPar = new ParameterBuilder(); + List pars = new ArrayList(); + + Parameter p1 = ticketPar.name("_tk_") + .description("token")//name表示名称,description表示描述 + .modelRef(new ModelRef("string")) + .parameterType("header") + .required(false) + .defaultValue("") + .build();//required表示是否必填,defaultvalue表示默认值 + + Parameter p2 = ticketPar.name("x-id") + .description("x-id")//name表示名称,description表示描述 + .modelRef(new ModelRef("string")) + .parameterType("header") + .required(false) + .defaultValue("") + .build();//required表示是否必填,defaultvalue表示默认值 + + pars.add(p1);//添加完此处一定要把下边的带***的也加上否则不生效 + pars.add(p2);//添加完此处一定要把下边的带***的也加上否则不生效 + + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .enable(swaggerEnabled) + .select() + .apis(basePackage(basePackageValue)) + .paths(PathSelectors.any()) + .build() + .ignoredParameterTypes(ApiIgnore.class, Session.class) + .globalOperationParameters(pars);//************把消息头添加 + } + + private ApiInfo apiInfo(){ + return new ApiInfoBuilder() + .title("sonic-lion") + .description("sonic lion API") + .version("1.0") + .contact(new Contact("sonic","","admin.sonic")) + .build(); + } + + public static Predicate basePackage(final String basePackage) { + return input -> declaringClass(input).transform(handlerPackage(basePackage)).or(true); + } + + private static Optional> declaringClass(RequestHandler input) { + return Optional.fromNullable(input.declaringClass()); + } + + private static Function, Boolean> handlerPackage(final String basePackage) { + return input -> { + // 循环判断匹配 + for (String strPackage : basePackage.split(SPLIT)) { + boolean isMatch = input.getPackage().getName().startsWith(strPackage); + if (isMatch) { + return true; + } + } + return false; + }; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/api/PayTradeApi.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/api/PayTradeApi.java new file mode 100644 index 0000000..45246be --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/api/PayTradeApi.java @@ -0,0 +1,170 @@ +package com.sonic.lion.controller.api; + +import com.sonic.common.auth.IgnoreAuth; +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.entity.AccountBuff; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.enums.SystemUser; +import com.sonic.lion.domain.input.PlatformGiftInput; +import com.sonic.lion.domain.input.PrePaymentInput; +import com.sonic.lion.domain.output.PrePaymentOutput; +import com.sonic.lion.domain.req.*; +import com.sonic.lion.domain.resp.BuffCheckoutResp; +import com.sonic.lion.domain.resp.PrePaymentResp; +import com.sonic.lion.domain.resp.QueryResp; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.ToastResultCode; +import com.sonic.lion.lib.input.GetAccountBuffInput; +import com.sonic.lion.lib.output.AccountBuffOutput; +import com.sonic.lion.service.BuffTransferService; +import com.sonic.lion.service.CheckOutService; +import com.sonic.lion.service.PayTradeService; +import com.sonic.sdk.api.annotation.InternalRpc; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.time.ZoneOffset; + +/** + * @author: code + * @date: 2025/05/11 + * @Description: + * @version: 1.0.0 + */ +@InternalRpc(path = "/api/**") +@RestController +public class PayTradeApi { + + @Autowired + private PayTradeService payTradeService; + @Autowired + private BuffTransferService buffTransferService; + @Autowired + private CheckOutService checkOutService; + + @ApiOperation(value = "创建账号钱包", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/trade/create-account-buff") + public Result createAccountBuff(@RequestBody GetAccountBuffInput input) { + //创建账号钱包 + buffTransferService.getAndInitAccount(input.getUid()); + return Result.success(); + } + + @ApiOperation(value = "获取用户余额", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/trade/get-account-buff") + public Result getAccountBuff(@RequestBody GetAccountBuffInput input) { + AccountBuffOutput output = new AccountBuffOutput(); + AccountBuff accountBuff = buffTransferService.getAndInitAccount(input.getUid()); + BeanUtils.copyProperties(accountBuff, output); + return Result.success(output); + } + + + @ApiOperation(value = "预下单", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/trade/pre-payment") + public Result prePayment(@RequestBody @Validated PrePaymentReq req) { + ToastResultCode.PARAM_ERROR.check(req.getDesAccountId() == null); + PrePaymentInput input = PrePaymentInput.builder() + .platform(req.getPlatform()) + .platformFee(req.getPlatformFee()) + .outTradeNo(req.getOutTradeNo()) + .outTradeNoRelationNo(req.getOutTradeNoRelationNo()) + .bizType(req.getBizType()) + .name(req.getName()) + .srcAccountId(req.getSrcAccountId()) + .desAccountNo(req.getDesAccountId().toString()) + .productAmount(req.getProductAmount()) + .promoAmount(req.getPromoAmount()) + .remark(req.getRemark()) + .srcAccountName("") + .closeTime(req.getCloseTime()) + .resourceKey(req.getResourceKey()) + .resourceNum(req.getResourceNum()) + .extend(req.getExtend()) + .payChannel(PayChannel.STRIPE) + .ip(req.getIp()) + .build(); + PrePaymentOutput output = payTradeService.prePayment(input); + PrePaymentResp resp = PrePaymentResp.builder() + .tradeNo(output.getTradeNo()) + .outTradeNo(output.getOutTradeNo()) + .outTradeNoRelationNo(output.getOutTradeNoRelationNo()) + .tradeStatus(output.getTradeStatus()) + .build(); + return Result.success(resp); + } + + @ApiOperation(value = "查询交易结果", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/trade/query") + public Result query(@RequestBody @Validated QueryReq req) { + PayTrade payTrade = payTradeService.getByPlatformAndOuterTradeNo(req.getPlatform(), req.getOutTradeNo()); + if (payTrade == null) { + return Result.success(); + } + QueryResp resp = new QueryResp(); + BeanUtils.copyProperties(payTrade, resp); + if (payTrade.getPayTime() != null) { + resp.setPayTime(payTrade.getPayTime().toInstant(ZoneOffset.of("+8")).toEpochMilli()); + } + return Result.success(resp); + } + + @ApiOperation(value = "退款", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/trade/refund") + public Result refund(@RequestBody @Validated RefundReq req) { + payTradeService.refund(req); + return Result.success(); + } + + @ApiOperation(value = "完成交易", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/trade/complete-trade") + public Result completeSecuredTrade(@RequestBody @Validated CompleteSecuredTradeReq req) { + payTradeService.completeSecuredTrade(req.getPlatform(), req.getOutTradeNo()); + return Result.success(); + } + + @ApiOperation(value = "关闭交易", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/trade/close-trade") + public Result closeTrade(@RequestBody @Validated CloseTradeReq req) { + payTradeService.closeTrade(req.getPlatform(), req.getOutTradeNo()); + return Result.success(); + } + + @ApiOperation(value = "结账给用户", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/trade/checkoutToUser") + public Result checkoutToUser(@RequestBody @Validated PrePaymentReq req) { + return Result.success(checkOutService.checkout(req)); + } + + @ApiOperation(value = "结账给平台", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/trade/checkoutToB") + public Result checkoutToB(@RequestBody @Validated PrePaymentReq req) { + //结账给系统用户 + req.setDesAccountId(SystemUser.SYSTEM_USER_1.getValue()); + return Result.success(checkOutService.checkout(req)); + } + + @ApiOperation(value = "平台赠送", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/trade/systemGift") + public Result systemGift(@RequestBody @Validated PlatformGiftInput req) { + PayTrade payTrade = payTradeService.platformGift(req); + Long amount = payTrade == null ? 0L : payTrade.getAmount(); + return Result.success(amount); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/api/SubscribeApi.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/api/SubscribeApi.java new file mode 100644 index 0000000..aab6bc9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/api/SubscribeApi.java @@ -0,0 +1,34 @@ +package com.sonic.lion.controller.api; + +import com.sonic.common.auth.IgnoreAuth; +import com.sonic.common.rpc.Result; +import com.sonic.lion.lib.input.GetSubscribeInput; +import com.sonic.lion.service.UserSubscriptionService; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * @author: code + * @date: 2025/05/11 + * @Description: + * @version: 1.0.0 + */ +@RestController +public class SubscribeApi { + + @Autowired + private UserSubscriptionService userSubscriptionService; + + @ApiOperation(value = "查询当前用户是否是会员", tags = "API-接口") + @IgnoreAuth + @PostMapping("/api/pay/subscribe/query") + public Result> queryUserSubscribe(@RequestBody GetSubscribeInput input) { + return Result.success(userSubscriptionService.queryUserIsSubscribe(input.getUserIdList())); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/mock/MockController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/mock/MockController.java new file mode 100644 index 0000000..503cf15 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/mock/MockController.java @@ -0,0 +1,78 @@ +package com.sonic.lion.controller.mock; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.input.SubChargeProductListInput; +import com.sonic.lion.domain.output.ChargeProductConfigOutput; +import com.sonic.lion.domain.output.EnabledPayChannelOutput; +import com.sonic.lion.domain.output.SubProductListOutput; +import com.sonic.lion.domain.req.PrePaymentReq; +import com.sonic.lion.domain.resp.BuffCheckoutResp; +import com.sonic.lion.service.ChargeProductConfigService; +import com.sonic.lion.service.CheckOutService; +import com.sonic.lion.service.PayChargeService; +import com.sonic.lion.service.PayConfigService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * @author: code + * @date: 2025/06/01 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@RestController +public class MockController { + + @Autowired + private CheckOutService checkOutService; + @Autowired + private PayChargeService payChargeService; + @Autowired + private PayConfigService payConfigService; + @Autowired + private ChargeProductConfigService chargeProductConfigService; + + @IgnoreAuth + @ApiOperation(value = "下单并结账", tags = "Mock-接口") + @PostMapping("/mock/pay/trade/checkout/v2") + public Result checkout(@RequestBody @Validated PrePaymentReq req) { + BuffCheckoutResp result = checkOutService.checkout(req); + return Result.success(result); + } + + @IgnoreAuth + @ApiOperation(value = "充值可用的支付渠道列表", tags = "支付相关配置") + @PostMapping("/mock/pay/config/enabled-pay-channel") + public Result enabledPayChannel(Session session) { + session.setUserId(1L); + return Result.success(payConfigService.enabledPayChannel(session.getUserId())); + } + + @IgnoreAuth + @ApiOperation(value = "充值档位列表", tags = "支付相关配置") + @PostMapping("/mock/pay/config/charge-product-list") + public Result chargeList(@RequestBody @Validated SubChargeProductListInput input, Session session) { + session.setUserId(1L); + ChargeProductConfigOutput output = chargeProductConfigService.getRechargeLevelConfig(session.getUserId(), input.getPlatform(), input.getVersion()); + return Result.success(output); + } + + @IgnoreAuth + @ApiOperation(value = "订阅档位列表", tags = "支付相关配置") + @PostMapping("/mock/pay/config/sub-product-list") + public Result> subProductList(@RequestBody @Validated SubChargeProductListInput input) { + return Result.success(payChargeService.subProductList(input)); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/probe/ProbeController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/probe/ProbeController.java new file mode 100644 index 0000000..5202dbb --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/probe/ProbeController.java @@ -0,0 +1,23 @@ +package com.sonic.lion.controller.probe; + +import com.sonic.common.auth.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 探测接口 + */ +@RestController +@Slf4j +public class ProbeController { + + @IgnoreAuth + @ApiOperation(value = "检测服务存活状态", tags = {"探针"}) + @PostMapping("/probe/check") + public void probeCheck() { + + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/AccountController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/AccountController.java new file mode 100644 index 0000000..9c59211 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/AccountController.java @@ -0,0 +1,53 @@ +package com.sonic.lion.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.input.BindBankCardInput; +import com.sonic.lion.domain.output.WalletOutput; +import com.sonic.lion.domain.req.BillListReq; +import com.sonic.lion.domain.resp.SummaryBillListResp; +import com.sonic.lion.service.AccountBuffBillService; +import com.sonic.lion.service.AccountBuffService; +import com.sonic.lion.service.PayAccountFundThirdService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * 支付账号 + */ +@RestController +public class AccountController { + + @Autowired + private AccountBuffService accountBuffService; + @Autowired + private AccountBuffBillService accountBuffBillService; + @Autowired + private PayAccountFundThirdService payAccountFundThirdService; + + @ApiOperation(value = "钱包余额", tags = "支付账号") + @PostMapping("/web/pay/account/wallet") + public Result wallet(@ApiParam(hidden = true) Session session) { + return Result.success(accountBuffService.wallet(session.getUserId())); + } + + @ApiOperation(value = "钱包流水", tags = "支付账号") + @PostMapping("/web/pay/account/bill-list") + public Result billListSummary(@RequestBody @Validated BillListReq req, @ApiParam(hidden = true) Session session) { + return Result.success(accountBuffBillService.billListSummary(req, session)); + } + + @ApiOperation(value = "绑定银行卡", tags = "支付账号") + @PostMapping("/web/pay/account/bind-bank-card") + public Result bindBankCard(@RequestBody @Validated BindBankCardInput input, @ApiParam(hidden = true) Session session) { + input.setUid(session.getUserId()); + payAccountFundThirdService.bindBankCard(input); + return Result.success(); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/CashierController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/CashierController.java new file mode 100644 index 0000000..549f982 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/CashierController.java @@ -0,0 +1,45 @@ +package com.sonic.lion.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.req.CheckoutReq; +import com.sonic.lion.domain.req.TradeQueryReq; +import com.sonic.lion.domain.resp.CheckoutResp; +import com.sonic.lion.domain.resp.QueryResp; +import com.sonic.lion.service.CashierService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * 收银台 + * @author: code + * @date: 2025/05/20 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@RestController +public class CashierController { + + @Autowired + private CashierService cashierService; + + @ApiOperation(value = "结账", tags = "收银台") + @PostMapping("/web/pay/cashier/checkout") + public Result checkout(@RequestBody @Validated CheckoutReq req, @ApiParam(hidden = true) Session session) throws Exception { + return Result.success(cashierService.checkout(session.getUserId(), req)); + } + + @ApiOperation(value = "结账结果查询", tags = "收银台") + @PostMapping("/web/pay/cashier/query") + public Result query(@RequestBody TradeQueryReq req) { + return cashierService.query(req); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/MemberController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/MemberController.java new file mode 100644 index 0000000..56fe099 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/MemberController.java @@ -0,0 +1,44 @@ +package com.sonic.lion.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.input.CreateSubscribeCheckSessionInput; +import com.sonic.lion.domain.input.UploadReceiptInput; +import com.sonic.lion.domain.output.CreateSubscribeCheckoutSessionOutput; +import com.sonic.lion.domain.output.MemberDetailOutput; +import com.sonic.lion.domain.req.UploadReceiptReq; +import com.sonic.lion.service.MemberService; +import com.sonic.lion.service.ReceiptHandlerService; +import com.sonic.lion.service.StripeSubscribeService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * 用户会员 + * + * @author: mzc + * @date: 2025/09/16 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@RestController +public class MemberController { + + @Autowired + private MemberService memberService; + + @ApiOperation(value = "会员详情", tags = "会员") + @PostMapping("/web/member/detail") + public Result memberDetail(@ApiParam(hidden = true) Session session) { + return Result.success(memberService.memberDetail(session.getUserId())); + } + + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/PayConfigController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/PayConfigController.java new file mode 100644 index 0000000..3604740 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/PayConfigController.java @@ -0,0 +1,59 @@ +package com.sonic.lion.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.input.SubChargeProductListInput; +import com.sonic.lion.domain.output.ChargeProductConfigOutput; +import com.sonic.lion.domain.output.EnabledPayChannelOutput; +import com.sonic.lion.domain.output.SubProductListOutput; +import com.sonic.lion.service.ChargeProductConfigService; +import com.sonic.lion.service.PayChargeService; +import com.sonic.lion.service.PayConfigService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * 支付相关配置 + * @author: code + * @date: 2025/06/22 + * @Description: + * @version: 1.0.0 + */ +@RestController +public class PayConfigController { + + @Autowired + private PayChargeService payChargeService; + @Autowired + private PayConfigService payConfigService; + @Autowired + private ChargeProductConfigService chargeProductConfigService; + + @ApiOperation(value = "充值可用的支付渠道列表", tags = "支付相关配置") + @PostMapping("/web/pay/config/enabled-pay-channel") + public Result enabledPayChannel(@ApiParam(hidden = true) Session session) { + return Result.success(payConfigService.enabledPayChannel(session.getUserId())); + } + + @ApiOperation(value = "充值档位列表", tags = "支付相关配置") + @PostMapping("/web/pay/config/charge-product-list") + public Result chargeList(@RequestBody @Validated SubChargeProductListInput input, @ApiParam(hidden = true) Session session) { + ChargeProductConfigOutput output = chargeProductConfigService.getRechargeLevelConfig(session.getUserId(), input.getPlatform(), input.getVersion()); + return Result.success(output); + } + + @ApiOperation(value = "订阅档位列表", tags = "支付相关配置") + @PostMapping("/web/pay/config/sub-product-list") + public Result> subProductList(@RequestBody @Validated SubChargeProductListInput input) { + return Result.success(payChargeService.subProductList(input)); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/PreChargeController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/PreChargeController.java new file mode 100644 index 0000000..198bfc7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/PreChargeController.java @@ -0,0 +1,53 @@ +package com.sonic.lion.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.IpAddressUtils; +import com.sonic.lion.domain.req.PreChargeReq; +import com.sonic.lion.domain.resp.PrePaymentResp; +import com.sonic.lion.service.PreChargeHandlerService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * 充值预下单 + * @author: code + * @date: 2025/06/22 + * @Description: + * @version: 1.0.0 + */ +@RestController +public class PreChargeController { + + @Autowired + private PreChargeHandlerService preChargeHandlerService; + + @ApiOperation(value = "web充值预下单", tags = "充值预下单") + @PostMapping("/web/pay/trade/pre-charge") + public Result preCharge(@RequestBody @Validated PreChargeReq req, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest httpServletRequest) { + PrePaymentResp resp = preChargeHandlerService.preCharge(session.getUserId(), req, IpAddressUtils.getIpAddress(httpServletRequest)); + return Result.success(resp); + } + + @ApiOperation(value = "iOS充值预下单", tags = "充值预下单") + @PostMapping("/web/pay/trade/pre-charge-iap") + public Result iapPreCharge(@RequestBody @Validated PreChargeReq req, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest httpServletRequest) { + PrePaymentResp resp = preChargeHandlerService.iapPreCharge(session.getUserId(), req, IpAddressUtils.getIpAddress(httpServletRequest)); + return Result.success(resp); + } + + @ApiOperation(value = "Google充值预下单", tags = "充值预下单") + @PostMapping("/web/pay/trade/pre-charge-google") + public Result googlePreCharge(@RequestBody @Validated PreChargeReq req, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest httpServletRequest) { + PrePaymentResp resp = preChargeHandlerService.googlePreCharge(session.getUserId(), req, IpAddressUtils.getIpAddress(httpServletRequest)); + return Result.success(resp); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/SubscribeController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/SubscribeController.java new file mode 100644 index 0000000..35020bd --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/SubscribeController.java @@ -0,0 +1,56 @@ +package com.sonic.lion.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.input.CreateSubscribeCheckSessionInput; +import com.sonic.lion.domain.input.UploadReceiptInput; +import com.sonic.lion.domain.output.CreateSubscribeCheckoutSessionOutput; +import com.sonic.lion.domain.req.UploadReceiptReq; +import com.sonic.lion.service.ReceiptHandlerService; +import com.sonic.lion.service.StripeSubscribeService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * 订阅相关 + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@RestController +public class SubscribeController { + + @Autowired + private ReceiptHandlerService receiptHandlerService; + @Autowired + private StripeSubscribeService stripeSubscribeService; + + @ApiOperation(value = "获取Stripe的订阅付款链接", tags = "订阅相关") + @PostMapping("/web/pay/subscribe/stripe-checkout") + public Result stripeSubCheckout(@RequestBody @Validated CreateSubscribeCheckSessionInput input, @ApiParam(hidden = true) Session session) { + return Result.success(stripeSubscribeService.createSubscribeCheckoutSession(session.getUserId(), input)); + } + + @ApiOperation(value = "上传IOS订阅收据", tags = "订阅相关") + @PostMapping("/web/pay/subscribe/upload-apple-receipt") + public Result uploadIosReceipt(@RequestBody @Validated UploadReceiptInput input, @ApiParam(hidden = true) Session session) { + receiptHandlerService.uploadIosSubReceipt(session.getUserId(), input); + return Result.success(); + } + + @ApiOperation(value = "上传Google订阅收据", tags = "订阅相关") + @PostMapping("/web/pay/subscribe/upload-google-receipt") + public Result uploadGoogleReceipt(@RequestBody @Validated UploadReceiptReq req, @ApiParam(hidden = true) Session session) { + receiptHandlerService.uploadGoogleSubReceipt(session.getUserId(), req); + return Result.success(); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WebhookAppleController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WebhookAppleController.java new file mode 100644 index 0000000..c6912f5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WebhookAppleController.java @@ -0,0 +1,36 @@ +package com.sonic.lion.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.req.IapUploadReceiptReq; +import com.sonic.lion.service.IapService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import static com.sonic.common.rpc.Result.success; + +/** + * Apple回调 + * @author: code + */ +@Slf4j +@RestController +public class WebhookAppleController { + + @Autowired + private IapService iapService; + + @ApiOperation(value = "iOS前端上传收据", tags = "Apple回调") + @PostMapping("/web/pay/webhooks/iap") + public Result iap(@RequestBody @Validated IapUploadReceiptReq req, @ApiParam(hidden = true) Session session) { + iapService.uploadReceipt(req, session.getUserId()); + return success(); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WebhookGoogleController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WebhookGoogleController.java new file mode 100644 index 0000000..e4badbc --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WebhookGoogleController.java @@ -0,0 +1,36 @@ +package com.sonic.lion.controller.web; + +import com.sonic.common.auth.IgnoreAuth; +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.req.GoogleUploadReceiptReqV2; +import com.sonic.lion.service.WebhookHandlerService; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import static com.sonic.common.rpc.Result.success; + +/** + * Google回调 + * @author: code + */ +@Slf4j +@RestController +public class WebhookGoogleController { + + @Autowired + private WebhookHandlerService webhookHandlerService; + + @IgnoreAuth + @ApiOperation(value = "Google前端上传收据", tags = "Google回调") + @PostMapping("/web/pay/webhooks/google") + public Result google(@RequestBody @Validated GoogleUploadReceiptReqV2 req) { + webhookHandlerService.googleHandler(req); + return success(); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WebhookStripeController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WebhookStripeController.java new file mode 100644 index 0000000..68b99e0 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WebhookStripeController.java @@ -0,0 +1,62 @@ +package com.sonic.lion.controller.web; + +import com.sonic.common.rpc.Result; +import com.sonic.lion.service.WebhookHandlerService; +import com.sonic.lion.utils.HttpUtil; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +import static com.sonic.common.rpc.Result.success; + +/** + * Stripe回调 + * @author: code + */ +@Slf4j +@RestController +public class WebhookStripeController { + + @Autowired + private WebhookHandlerService webhookHandlerService; + + @IgnoreAuth + @ApiOperation(value = "stripe的充值回调", tags = {"Stripe回调"}) + @PostMapping(value = "/web/pay/webhooks/stripe/payment") + public Result payment(@RequestHeader("Stripe-Signature") String signature, @ApiParam(hidden = true) HttpServletRequest request) throws Exception { + webhookHandlerService.stripePaymentHandler(signature, HttpUtil.getBody(request)); + return success(); + } + + @IgnoreAuth + @ApiOperation(value = "stripe的提现回调", tags = {"Stripe回调"}) + @PostMapping(value = "/web/pay/webhooks/stripe/payout") + public Result payout(@RequestHeader("Stripe-Signature") String signature, @ApiParam(hidden = true) HttpServletRequest request) throws Exception { + webhookHandlerService.stripePayoutHandler(signature, HttpUtil.getBody(request)); + return success(); + } + + @IgnoreAuth + @ApiOperation(value = "stripe的订阅回调", tags = {"Stripe回调"}) + @PostMapping(value = "/web/pay/webhooks/stripe/subscription") + public Result subscription(@RequestHeader("Stripe-Signature") String signature, @ApiParam(hidden = true) HttpServletRequest request) throws Exception { + webhookHandlerService.stripeSubscriptionHandler(signature, HttpUtil.getBody(request)); + return success(); + } + + @IgnoreAuth + @ApiOperation(value = "stripe的争议回调", tags = {"Stripe回调"}) + @PostMapping(value = "/web/pay/webhooks/stripe/dispute") + public Result dispute(@RequestHeader("Stripe-Signature") String signature, @ApiParam(hidden = true) HttpServletRequest request) throws Exception { + webhookHandlerService.stripeDisputeHandler(signature, HttpUtil.getBody(request)); + return success(); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WithdrawController.java b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WithdrawController.java new file mode 100644 index 0000000..10f8a1e --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/controller/web/WithdrawController.java @@ -0,0 +1,232 @@ +package com.sonic.lion.controller.web; + +import com.google.common.collect.Lists; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.RedisLock; +import com.sonic.lion.domain.entity.PayAccountFundThird; +import com.sonic.lion.domain.entity.WithdrawRequest; +import com.sonic.lion.domain.enums.CoinType; +import com.sonic.lion.domain.resp.GetPayAccountFundThirdResp; +import com.sonic.lion.enums.*; +import com.sonic.lion.service.PayAccountFundThirdService; +import com.sonic.lion.service.PayConfigService; +import com.sonic.lion.service.WithdrawService; +import com.sonic.lion.utils.MaskUtils; +import com.sonic.lion.utils.RedisKeyUtils; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.annotations.ApiIgnore; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author: code + * @date: 2025/06/22 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@RestController +public class WithdrawController { + + @Autowired + private PayAccountFundThirdService payAccountFundThirdService; + @Autowired + private WithdrawService withdrawService; + @Autowired + private PayConfigService payConfigService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @ApiOperation(value = "提现", tags = "提现") + @PostMapping("/web/pay/trade/withdraw") + public Result withdraw(@ApiParam @RequestBody @Validated WithdrawReq req, @ApiParam(hidden = true) Session session) { + //加锁,避免并发重复处理 + RedisLock redisLock = new RedisLock(redisKeyUtils.withdrawLockKey(session.getUserId()), redisWrapper); + redisLock.tryAcquireRun(60 * 1000, () -> { + //校验渠道开关是否打开 + boolean channelSwitchBl = payConfigService.payoutChannelSwitchCheckPass(session.getUserId(), req.getPayChannel()); + ToastResultCode.CHANNEL_NOT_OPEN.check(!channelSwitchBl); + + PayAccountFundThird payAccountFundThird = payAccountFundThirdService.getByAccountIdAndAppType(session.getUserId(), ThirdAccountType.getWithdrawAccount(req.getPayChannel())); + ToastResultCode.NO_THIRD_PARTY_ACCOUNT.check(payAccountFundThird == null); + + String desAccountNo = payAccountFundThird.getOpenId(); + ToastResultCode.THIRD_PARTY_ACCOUNT_UNBIND.check(StringUtils.isBlank(desAccountNo)); + + //针对Airwallex渠道,获取用户的目标货币类型 + String currencyType = null; + if(ThirdAccountType.STRIPE_PAYOUT == payAccountFundThird.getAppType()) { +// //Airwallex渠道提现,但是扩展字段值为空的话直接抛出异常 +// ToastResultCode.THIRD_PARTY_ACCOUNT_UNBIND.check(StringUtils.isBlank(payAccountFundThird.getExtend())); +// //查询三方渠道表获取渠道目标货币类型 +// CreateBeneficiaryOutput userBeneficiary = payAccountFundThirdService.getAirwallexPayOutBaseInfo(payAccountFundThird.getExtend()); +// //设置收款人的货币类型 +// currencyType = userBeneficiary.getAccountCurrency(); +// //Airwallex渠道提现,但是货币类型为空的话直接抛出异常 +// ToastResultCode.THIRD_PARTY_ACCOUNT_UNBIND.check(StringUtils.isBlank(currencyType)); + } + //金额判断 + withdrawService.checkAmount(req.getPayChannel(),req.getAmount(),session.getUserId()); + + //提现发起 将字段 挪动到 提现冻结中 。 + //提现失败 从冻结中 回滚 到 可提现账户 状态 失败。 + WithdrawRequest input = WithdrawRequest.builder() + .platform(Platform.PAY.getDesc()) + .payChannel(req.getPayChannel().getValue()) + .name(BizType.WITHDRAW.getDesc()) + .srcAccountId(session.getUserId()) + .srcAccountName("") + .desAccountNo(desAccountNo) + .desAccountName(payAccountFundThird.getName()) + .amount(req.getAmount()) + .remark(req.getRemark()) + .coinType(req.getCoinType()) + .currencyType(currencyType) + .createTime(LocalDateTime.now()) + .build(); + if (req.getBindDataType() != null) { + input.setDesAccountType(req.getBindDataType().getValue()); + } + + withdrawService.withdrawReview(input); + return true; + }); + return Result.success(); + } + + @ApiOperation(value = "渠道列表", tags = "提现输入申请页面") + @PostMapping("/web/pay/trade/withdraw/channel-list") + public Result Channel(@ApiParam(hidden = true) Session session) { + List payoutChannels ; + List debugUserIds = payConfigService.getDebugPayUserIds(); + if(debugUserIds != null && debugUserIds.contains(session.getUserId().toString())) { + payoutChannels = Lists.newArrayList(PayChannel.STRIPE.name()); + } else { + payoutChannels = payConfigService.getEnabledPayOutChannel(); + } + if(payoutChannels.isEmpty()){ + return Result.success(new ArrayList<>()); + } + List result = new ArrayList<>(payoutChannels.size()) ; + for (String payoutChannel : payoutChannels) { + WithdrawChannelResp withdrawChannelResp = new WithdrawChannelResp(); + withdrawChannelResp.setPayChannel(payoutChannel); + PayAccountFundThird payAccountFundThird = payAccountFundThirdService.getByAccountIdAndAppType(session.getUserId(), ThirdAccountType.getWithdrawAccount(PayChannel.valueOf(payoutChannel))); + withdrawChannelResp.setIsBind(payAccountFundThird!=null); + if(payAccountFundThird != null){ + payAccountFundThird.setEmail(MaskUtils.maskEmail(payAccountFundThird.getEmail())); + } + result.add(withdrawChannelResp); + } + return Result.success(result); + } + + + @ApiOperation(value = "获取第三方提现账号信息") + @PostMapping(value = "/web/pay/get_third_payout_account") + public Result> getThirdPayoutAccount(@ApiParam(hidden = true) Session session) { + log.info("get_third_payout_account userId:{}" ,session.getUserId()); + List payoutChannels ; + List debugUserIds = payConfigService.getDebugPayUserIds(); + if (debugUserIds != null && debugUserIds.contains(session.getUserId().toString())) { + payoutChannels = Lists.newArrayList(PayChannel.STRIPE.name()); + }else{ + payoutChannels = payConfigService.getEnabledPayOutChannel(); + } + Map data = new HashMap<>(2); + data.put("payoutChannel" , payoutChannels); + return Result.success(data); + } + + + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class WithdrawReq { + + /** + * 渠道ID + */ + @NotNull + @ApiModelProperty("支付渠道") + private PayChannel payChannel; + + /** + * 交易金额,单位:分 + */ + @NotNull + @Min(value = 3000, message = "Must be greater than or equal to 30") + @Max(value = 100000, message = "must be less than or equal to 1000") + @ApiModelProperty("交易金额,单位:分") + private Long amount; + + + /** + * 备注 + */ + @ApiModelProperty("备注") + private String remark; + + /** + * payChannel为braintree等聚合支付渠道时,转账需要选择所绑定的具体账户类型 + */ + @ApiModelProperty("payChannel为braintree等聚合支付渠道时,转账需要选择所绑定的具体账户类型") + private BindDataType bindDataType; + + /** + * 提现币类型 + */ + @ApiModelProperty("提现币类型,默认BUFF") + private CoinType coinType = CoinType.BUFF; + + + @NotBlank(message = "checkCode must not be empty") + @ApiParam(value = "验证码", required = true) + String checkCode; + + } + + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class WithdrawChannelResp { + + @ApiModelProperty("支付渠道") + private String payChannel; + + @ApiModelProperty("是否绑定") + private Boolean isBind; + + @ApiModelProperty("提现绑定的国家(是否是美国United States)") + private Boolean isUs; + + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/AccountBuffAwaitingDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/AccountBuffAwaitingDao.java new file mode 100644 index 0000000..3e445a5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/AccountBuffAwaitingDao.java @@ -0,0 +1,22 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.AccountBuffAwaiting; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +public interface AccountBuffAwaitingDao extends BaseMapper { + + /** + * 查询将要入账的记录 + * + * @param num + * @return + */ + List selectWillToIncome(@Param("num") Integer num); + + @Update("UPDATE account_buff_awaiting SET to_withdrawable_income_time=NOW() WHERE fronzen_status='UN_FRONZEN' AND STATUS =0 AND to_withdrawable_income_time < DATE_SUB( NOW() ,INTERVAL 3 DAY)") + int updateOldData(); +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/AccountBuffBillDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/AccountBuffBillDao.java new file mode 100644 index 0000000..aae8372 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/AccountBuffBillDao.java @@ -0,0 +1,105 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.AccountBuffBill; +import com.sonic.lion.domain.entity.PayTrade; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.time.LocalDateTime; +import java.util.List; + +public interface AccountBuffBillDao extends BaseMapper { + + /** + * 更新bill 状态 + * + * @param tradeNo + * @param status + * @param message + * @return + */ + @Update("update account_buff_bill set bill_status =#{status} ,withdraw_status='WITHDRAW_FAIL' , reason=#{message} where trade_no=#{tradeNo} AND biz_type =500 ") + int updateStatus(@Param("tradeNo") String tradeNo, @Param("status") int status, @Param("message") String message); + + + Long sumTotal(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("uid") Long uid, @Param("inOrOut") Integer inOrOut, @Param("type") String type, @Param("bizType") String bizType); + + + /** + * 消费 汇总 + * + * @param startTime + * @param endTime + * @param uid + * @return + */ + Long sumConsumption(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("uid") Long uid); + + + /** + * 更新提现流水 的状态 和 原因。 + * + * @param id + * @param status + * @param message + * @return + */ + @Update("update account_buff_bill set withdraw_status =#{status} , reason=#{message} where id=#{id} ") + int updateWithdrawStatus(@Param("id") Long id, @Param("status") String status, @Param("message") String message); + + + @Update("update account_buff_bill set balance= balance - #{amount} where id=#{id} ") + int updateWithdrawBillAmount(@Param("id") Long id, @Param("amount") Long amount); + + /** + * 列出 某时间 之前的 审核中的 bill + * + * @param beginTime + * @param endTime + * @return + */ + @Select("SELECT t1.* FROM t_pay_trade t1 JOIN account_buff_bill t2 ON t1.trade_no = t2.`trade_no`\n" + + "WHERE t2.biz_type= 500 AND t2.withdraw_status= 'IN_REVIEW' AND t2.create_time < #{endTime} AND t2.create_time > #{beginTime}") + List listTimeOutInReviewWithdrawBill(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); + + /** + * 根据 tradeNo 更新 提现状态 + * + * @param tradeNo + * @param withdrawSuccess + * @return + */ + @Update("update account_buff_bill set withdraw_status =#{status} where trade_no=#{tradeNo} AND biz_type = 500") + int updateWithdrawStatusByTradeNo(@Param("tradeNo") String tradeNo, @Param("status") AccountBuffBill.WithdrawStatus withdrawSuccess); + + + /** + * 修改 bill 金额。 + * + * @param tradeNo + * @param amount + */ + @Update("update account_buff_bill set buff=#{amount} where trade_no=#{tradeNo} AND uid = #{uid} AND in_or_out = 1") + void updateAmountByTradeNo(@Param("tradeNo") String tradeNo, @Param("uid") Long uid, @Param("amount") Long amount); + + /** + * 根据 tradeNo 更新 提现状态 + * + * @param buffType + * @param billId + * @return + */ + @Update("update account_buff_bill set buff_type =#{buffType} where id=#{billId}") + int updateWithdrawableIncome(@Param("buffType") Integer buffType, @Param("billId") Long billId); + + /** + * 统计用户在PST当天充值的Buff总金额 + * @param userId + * @param startTime + * @return + */ + Long statPstCurrentDayChargeBuff(@Param("userId") Long userId, @Param("startTime") LocalDateTime startTime); + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/AccountBuffDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/AccountBuffDao.java new file mode 100644 index 0000000..ab810e9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/AccountBuffDao.java @@ -0,0 +1,176 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.AccountBuff; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +public interface AccountBuffDao extends BaseMapper { + + /** + * 通过uid和支付渠道查询账户id + * + * @param uid + * @return + */ + Long selectIdByUid(@Param("uid") Long uid); + + /** + * 增加可提现收入 + * + * @param id + * @param buff + * @return + */ + int addWithdrawableIncome(@Param("id") Long id, @Param("buff") Long buff); + + /** + * 减少指定账户余额 + * + * @param id + * @param buff + * @return + */ + int decWithdrawableIncome(@Param("id") Long id, @Param("buff") Long buff); + + + /** + * 冻结资金 + * + * @param id + * @param buff + * @return + */ + int withdrawableIncomeToFrozenIncome(@Param("id") Long id, @Param("buff") Long buff); + + /** + * 扣除冻结资金 + * + * @param id + * @param buff + * @return + */ + int decFrozenIncome(@Param("id") Long id, @Param("buff") Long buff); + + + /** + * 增加用户待入账金额 + * + * @param id + * @param buff + * @return + */ + int addAwaitingIncome(@Param("id") Long id, @Param("buff") Long buff); + + /** + * 待入账资金入账 + * + * @param id + * @param buff + * @return + */ + int awaitingIncomeToWithdrawableIncome(@Param("id") Long id, @Param("buff") Long buff); + + /** + * 扣减待入账金额 + * + * @param id + * @param buff + * @return + */ + int decAwaitingIncome(@Param("id") Long id, @Param("buff") Long buff); + + /** + * 增加指定账户充值金额 + * + * @param id + * @param buff + * @return + */ + int addBalance(@Param("id") Long id, @Param("buff") Long buff); + + /** + * 减少指定账户充值金额 + * + * @param id + * @param buff + * @return + */ + int decBalance(@Param("id") Long id, @Param("buff") Long buff); + + + /** + * 冻结余额 + * + * @param id + * @param buff + * @return + */ + @Update(" update account_buff set balance = balance - #{buff} , frozen_balance = frozen_balance+ #{buff} where uid = #{id} and balance >= #{buff}") + int frozenBalance(@Param("id") Long id, @Param("buff") Long buff); + + /** + * 解冻 + * + * @param id + * @param buff + * @return + */ + @Update("update account_buff set balance = balance + #{buff} , frozen_balance = frozen_balance - #{buff} where uid = #{id} and frozen_balance >= #{buff}") + int unFrozenBalance(@Param("id") Long id, @Param("buff") Long buff); + + + /** + * 增加总充值金额 + * + * @param id + * @param buff + * @return + */ + @Update("update account_buff set recharge_total = recharge_total + #{buff} where uid = #{id}") + int addRechargeTotal(@Param("id") Long id, @Param("buff") Long buff); + + + /** + * 查询总充值表 + * + * @return + */ + @Select("SELECT uid AS uId, SUM(buff) AS rechargeTotal FROM account_buff_bill WHERE biz_type = 600 AND in_or_out =1 GROUP BY uid") + List getHistroyRechargeTotal(); + + + /** + * 增加提现 在途金额 + * + * @param id + * @param buff + * @return 条数 + */ + @Update("update account_buff set withdraw_on_going = withdraw_on_going + #{buff} where id = #{id}") + int addWithdrawOnGoing(@Param("id") Long id, @Param("buff") Long buff); + + + /** + * 减少 提现在途金额 + * + * @param id + * @param buff + * @return + */ + @Update("update account_buff set withdraw_on_going = withdraw_on_going - #{buff} where id = #{id} AND withdraw_on_going >= #{buff}") + int decWithdrawOnGoing(@Param("id") Long id, @Param("buff") Long buff); + + + /** + * 根据uid 查询账户 + * + * @param uid + * @return + */ + @Select("select * from account_buff where uid=#{uid} limit 1") + AccountBuff selectByUid(@Param("uid") Long uid); +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/AppStoreProductDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/AppStoreProductDao.java new file mode 100644 index 0000000..96a9904 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/AppStoreProductDao.java @@ -0,0 +1,19 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.AppStoreProduct; +import com.sonic.lion.domain.resp.AppProductInfoOutput; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +public interface AppStoreProductDao extends BaseMapper { + + /** + * 查询app产品信息 + * @param appProductId + * @return + */ + @Select("select * from t_app_product where product_id = #{appProductId} and is_delete = 0") + AppProductInfoOutput queryAppProductInfo(@Param("appProductId") String appProductId); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/AppleRefundRecordDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/AppleRefundRecordDao.java new file mode 100644 index 0000000..2f70d64 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/AppleRefundRecordDao.java @@ -0,0 +1,7 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.AppleRefundRecord; + +public interface AppleRefundRecordDao extends BaseMapper { +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/BuffRewardRecordDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/BuffRewardRecordDao.java new file mode 100644 index 0000000..bdc7eb6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/BuffRewardRecordDao.java @@ -0,0 +1,21 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.BuffRewardRecord; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; + +public interface BuffRewardRecordDao extends BaseMapper { + + /** + * 查询 本月累计充值buff 数 + * + * @param firstday + * @param lastDay + * @return + */ + @Select("SELECT IFNULL(SUM(buff),0)-IFNULL(SUM(gift_amount),0) FROM account_buff_bill WHERE uid =#{uid} AND biz_type = 600 AND in_or_out =1 AND create_time BETWEEN #{begin} AND #{end} ") + Long queryTotalChargeAmount(@Param("uid")Long uid, @Param("begin") LocalDateTime firstday, @Param("end") LocalDateTime lastDay); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/ChannelBlacklistDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/ChannelBlacklistDao.java new file mode 100644 index 0000000..3d99818 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/ChannelBlacklistDao.java @@ -0,0 +1,17 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.ChannelBlacklist; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +public interface ChannelBlacklistDao extends BaseMapper { + + @Select("select count(1) from channel_blacklist where user_id =#{payerId} and channel_type = #{channelType} and is_delete = 0") + int exitsPayerId(@Param("payerId") Long payerId, @Param("channelType") String channelType); + + + @Select("select * from channel_blacklist where user_id =#{payerId} and channel_type = #{channelType} limit 1") + ChannelBlacklist getPayerId(@Param("payerId") Long payerId, @Param("channelType") String channelType); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/FreeWithdrawConfigDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/FreeWithdrawConfigDao.java new file mode 100644 index 0000000..05e2169 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/FreeWithdrawConfigDao.java @@ -0,0 +1,20 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.FreeWithdrawConfig; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +public interface FreeWithdrawConfigDao extends BaseMapper { + + /** + * 查询根据 结果排序 + * + * @param uid + * @return + */ + @Select("SELECT * FROM t_free_withdraw_config WHERE uid=#{uid} AND is_delete= 0 ORDER BY FIELD(reason,\"ANGEL_USER\",\"OPERATION\",\"SHOP_LEVEL\")") + List listOrderByEndTime(@Param("uid") Long uid); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/FreeWithdrawFeeBillDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/FreeWithdrawFeeBillDao.java new file mode 100644 index 0000000..a16aa42 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/FreeWithdrawFeeBillDao.java @@ -0,0 +1,7 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.FreeWithdrawFeeBill; + +public interface FreeWithdrawFeeBillDao extends BaseMapper { +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/GoogleRecordDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/GoogleRecordDao.java new file mode 100644 index 0000000..5f5d244 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/GoogleRecordDao.java @@ -0,0 +1,7 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.GoogleRecord; + +public interface GoogleRecordDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/GoogleUploadReceiptDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/GoogleUploadReceiptDao.java new file mode 100644 index 0000000..59c693d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/GoogleUploadReceiptDao.java @@ -0,0 +1,13 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.GoogleUploadReceipt; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface GoogleUploadReceiptDao extends BaseMapper { + + List list(@Param("processed") Boolean processed, @Param("startCreateTime") LocalDateTime startCreateTime, @Param("num") Integer num); +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/IapRecordDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/IapRecordDao.java new file mode 100644 index 0000000..530810d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/IapRecordDao.java @@ -0,0 +1,7 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.IapRecord; + +public interface IapRecordDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/IapUploadReceiptDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/IapUploadReceiptDao.java new file mode 100644 index 0000000..3affd3a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/IapUploadReceiptDao.java @@ -0,0 +1,13 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.IapUploadReceipt; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface IapUploadReceiptDao extends BaseMapper { + + List list(@Param("processed") Boolean processed, @Param("startCreateTime") LocalDateTime startCreateTime, @Param("num") Integer num); +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/MemberPrivDictDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/MemberPrivDictDao.java new file mode 100644 index 0000000..be2a15e --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/MemberPrivDictDao.java @@ -0,0 +1,13 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.MemberPrivDict; +import org.apache.ibatis.annotations.Mapper; + +/** + * 会员特权字典表 Mapper 接口 + */ +@Mapper +public interface MemberPrivDictDao extends BaseMapper { + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountBillDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountBillDao.java new file mode 100644 index 0000000..e47fa92 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountBillDao.java @@ -0,0 +1,8 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.PayAccountBill; + +public interface PayAccountBillDao extends BaseMapper { + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundAwaitingDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundAwaitingDao.java new file mode 100644 index 0000000..f221a55 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundAwaitingDao.java @@ -0,0 +1,18 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.PayAccountFundAwaiting; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface PayAccountFundAwaitingDao extends BaseMapper { + + /** + * 查询等待入账的记录 + * + * @param num + * @return + */ + List selectAwaitingToBalance( @Param("num") int num); +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundDao.java new file mode 100644 index 0000000..fedf947 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundDao.java @@ -0,0 +1,134 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.domain.entity.PayAccountFund; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +public interface PayAccountFundDao extends BaseMapper { + + /** + * 通过uid和支付渠道查询账户id + * + * @param uid + * @param payChannel + * @return + */ + Long selectAccountIdByUid(@Param("uid") Long uid, @Param("payChannel") PayChannel payChannel); + + /** + * 增加指定账户余额 + * + * @param accountId + * @param amount + * @return + */ + int addBalanceAmount(@Param("accountId") Long accountId, @Param("payChannel") PayChannel payChannel, @Param("amount") Long amount); + + /** + * 减少指定账户余额 + * + * @param accountId + * @param amount + * @return + */ + int decBalance(@Param("accountId") Long accountId, @Param("payChannel") PayChannel payChannel, @Param("amount") Long amount); + + /** + * 获取账户信息并加锁 + * + * @param payChannel + * @param accountId + * @return + */ + PayAccountFund selectByAccountIdAndPayChannelLock(@Param("accountId") Long accountId, @Param("payChannel") PayChannel payChannel); + + /** + * 冻结资金 + * + * @param id + * @param amount + * @return + */ + int frozen(@Param("id") Long id, @Param("amount") Long amount); + + /** + * 扣除冻结资金 + * + * @param id + * @param amount + * @return + */ + int decFreezeAmount(@Param("id") Long id, @Param("amount") Long amount); + + /** + * 根据id获取账户信息并加锁 + * + * @param id + * @return + */ + PayAccountFund selectByIdLock(@Param("id") Long id); + + /** + * 增加用户待入账金额 + * + * @param accountId + * @param payChannel + * @param amount + * @return + */ + int addAwaitingAmount(@Param("accountId") Long accountId, @Param("payChannel") PayChannel payChannel, @Param("amount") Long amount); + + /** + * 待入账资金入账 + * + * @param accountId + * @param payChannel + * @param amount + * @return + */ + int awaitingFundToBalance(@Param("accountId") Long accountId, @Param("payChannel") PayChannel payChannel, @Param("amount") Long amount); + + /** + * 扣减待入账金额 + * + * @param accountId + * @param payChannel + * @param amount + * @return + */ + int decAwaitingAmount(@Param("accountId") Long accountId, @Param("payChannel") PayChannel payChannel, @Param("amount") Long amount); + + /** + * 增加指定账户充值金额 + * + * @param accountId + * @param amount + * @return + */ + int addChargeAmount(@Param("accountId") Long accountId, @Param("payChannel") PayChannel payChannel, @Param("amount") Long amount); + + /** + * 减少指定账户充值金额 + * + * @param accountId + * @param amount + * @return + */ + int decChargeAmount(@Param("accountId") Long accountId, @Param("payChannel") PayChannel payChannel, @Param("amount") Long amount); + + + @Select("select a.balance,a.account_id As accountId,a.charge from t_pay_account_fund a, t_user b where (a.balance > 0 or a.charge >0 ) AND a.account_id = b.id AND a.status =1 AND b.account_status=1 AND a.account_id!=1") + List listAmountEnable(); + + @Update("update t_pay_account_fund set balance=0 , charge=0 where account_id =#{accountId}") + int removeOldMoney(@Param("accountId") Long accountId); + + + @Update("update account_buff set balance = balance+ #{charge} , withdrawable_income = withdrawable_income+#{balance} where uid =#{accountId}") + int updateBuff(@Param("accountId")Long accountId, @Param("balance")Long balance, @Param("charge")Long charge); +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundFrozenDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundFrozenDao.java new file mode 100644 index 0000000..828eedc --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundFrozenDao.java @@ -0,0 +1,24 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.PayAccountFundFrozen; +import org.apache.ibatis.annotations.Param; + +public interface PayAccountFundFrozenDao extends BaseMapper { + + /** + * 更改资金冻结记录为解冻状态 + * + * @param id + * @return + */ + int unfreeze(@Param("id") Long id); + + /** + * 根据交易号查询资金冻结记录 + * + * @param tradeNo + * @return + */ + PayAccountFundFrozen selectByTradeNo(@Param("tradeNo") String tradeNo); +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundThirdDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundThirdDao.java new file mode 100644 index 0000000..57d9c32 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountFundThirdDao.java @@ -0,0 +1,20 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.PayAccountFundThird; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +public interface PayAccountFundThirdDao extends BaseMapper { + + PayAccountFundThird selectByAccountFundId(@Param("accountFundId") Long accountFundId); + + @Delete("delete from t_pay_account_fund_third where account_id =#{accountId} AND app_type=#{appType}") + int deleteByAccountIdAndType(@Param("accountId") Long accountId,@Param("appType")Integer appType); + + + @Update("update t_pay_account_fund_third set email = #{email}, extend = #{extend} where id = #{id}") + void updateEmailAndExtend(@Param("id") Long id, @Param("email") String email, @Param("extend") String extend); + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountThirdBindDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountThirdBindDao.java new file mode 100644 index 0000000..5345be3 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayAccountThirdBindDao.java @@ -0,0 +1,7 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.PayAccountThirdBind; + +public interface PayAccountThirdBindDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayCallChannelRecordDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayCallChannelRecordDao.java new file mode 100644 index 0000000..83d8136 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayCallChannelRecordDao.java @@ -0,0 +1,77 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.domain.entity.PayCallChannelRecord; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.time.LocalDateTime; +import java.util.List; + +public interface PayCallChannelRecordDao extends BaseMapper { + + + @Select("select * from t_pay_call_channel_record where transaction_id =#{transactionId} ") + PayCallChannelRecord getByTransactionId(@Param("transactionId") String transactionId); + + @Select("select * from t_pay_call_channel_record where channel=#{channel} AND trade_no =#{tradeNo} limit 1 ") + PayCallChannelRecord getByTradeNoAndChannel(@Param("tradeNo") String tradeNo, @Param("channel") int channel); + + /** + * 更新 通道调用状态 + * + * @param id + * @param status + * @return + */ + int updateStatus(@Param("id") Long id, @Param("status") Integer status); + + + @Update("update t_pay_call_channel_record set transaction_id =#{transactionId},status=2 where trade_no=#{trade_no} AND channel=9") + int updateTransactionId(@Param("trade_no") String trade_no, @Param("transactionId") String transactionId); + + /** + * 根据tradeNo查询最后一条记录 + * + * @param tradeNo + * @param bizType + * @return + */ + PayCallChannelRecord selectByTradeNoAndBizTypeLast(@Param("tradeNo") String tradeNo, @Param("bizType") BizType bizType); + + /** + * 查询应该检查的记录: + * 状态为 INIT,PROCESSING, UNKNOW + * 下一次查询时间小于当前时间 + * 查询次数小于10次 + * + * @return + */ + List selectWillCheckRecord(@Param("startCreateTime") LocalDateTime startCreateTime); + + /** + * @param batchId + * @return + */ + PayCallChannelRecord selectByBatchId(@Param("batchId") String batchId); + + + /** + * 查询需要检查的 充值渠道 记录 + * + * @param startCreateTime + * @return + */ + List selectWillCheckRecordCharge(@Param("startCreateTime") LocalDateTime startCreateTime); + + /** + * 查询需要检查的 提现渠道记录 + * + * @param startCreateTime + * @return + */ + List selectWillCheckRecordWithdraw(@Param("startCreateTime") LocalDateTime startCreateTime); + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayChannelBillDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayChannelBillDao.java new file mode 100644 index 0000000..d605a83 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayChannelBillDao.java @@ -0,0 +1,8 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.PayChannelBill; + +public interface PayChannelBillDao extends BaseMapper { + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayChargeDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayChargeDao.java new file mode 100644 index 0000000..19511d2 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayChargeDao.java @@ -0,0 +1,7 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.PayCharge; + +public interface PayChargeDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayConfigDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayConfigDao.java new file mode 100644 index 0000000..e0c21dc --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayConfigDao.java @@ -0,0 +1,7 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.PayConfig; + +public interface PayConfigDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayTradeDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayTradeDao.java new file mode 100644 index 0000000..f355cb6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/PayTradeDao.java @@ -0,0 +1,149 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.input.RefundVipInput; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.Date; +import java.util.List; + +public interface PayTradeDao extends BaseMapper { + + /** + * 统计充值实付金额 + * + * @param uid + * @return + */ + long sumChargeOccurAmount(@Param("uid") long uid); + + /** + * 批量关闭交易(交易关闭时间小于当前时间的交易) + * + * @return + */ + int closeWaitPayTrade(); + + /** + * 根据资源key查询已付款的交易数并对加锁(除待付款和已关闭) + * + * @param resourceKey + * @return + */ + int countPaidByResourceKeyLock(String resourceKey); + + /** + * 根据资源key查询已付款的交易数(除待付款和已关闭) + * + * @param resourceKey + * @return + */ + int countPaidByResourceKey(String resourceKey); + + /** + * 根据用户uid和支付交易号查询最后一笔充值交易 + * + * @param srcUid + * @param paymentTradeNo + * @return + */ + PayTrade selectBySrcUidAndPaymentTradeNoLast(@Param("srcUid") Long srcUid, @Param("paymentTradeNo") String paymentTradeNo); + + PayTrade selectBySrcUidAndPaymentTradeNoAndTypeLast(@Param("srcUid") Long srcUid, @Param("paymentTradeNo") String paymentTradeNo, @Param("bizType") String bizType); + + + @Update("update t_pay_trade set error_message = #{errorMessage} where trade_no=#{tradeNo}") + int updateErrorMessage(@Param("errorMessage") String errorMessage, @Param("tradeNo") String tradeNo); + + @Update("update t_pay_trade set status=#{status} , error_message = #{errorMessage} where trade_no=#{tradeNo} AND status not in (2,4)") + int updateStatusErrorMessage(@Param("status") int status, @Param("errorMessage") String errorMessage, @Param("tradeNo") String tradeNo); + + @Select("select count(1) from t_pay_trade where srcAccountId = #{srcAccountId} AND gift_amount>0 ") + int isThanksGivingDayExits(Long srcAccountId); + + + @Update("update t_pay_trade set standbox = #{standbox} where id=#{id}") + int setStanbox(@Param("standbox") Integer standbox, @Param("id") Long id); + + @Select("SELECT count(1) from t_pay_trade where des_account_no = #{userId} AND biz_type=200 ") + int countWelcomeGift(@Param("userId") Long userId); + + @Select("SELECT count(1) from t_pay_trade where des_account_no = #{userId} AND biz_type=207 ") + int countBuffGift(@Param("userId") Long userId); + + @Select("SELECT count(1) from t_pay_trade where ip = #{ip} AND biz_type=207 ") + int countBuffGiftByIp(@Param("ip") String ip); + + @Select(" SELECT COUNT(1) FROM t_pay_trade t where t.des_account_no = #{userId} AND t.biz_type=207 ") + Integer countNewUserGiftBuff(@Param("userId") Long id); + + + /** + * 用户总充值 + * + * @param id + * @return + */ + @Select(" SELECT IFNULL(SUM(amount),0) FROM t_pay_trade t WHERE t.src_account_id = #{userId} AND t.biz_type=600 AND channel_id=6") + Integer countTotalBuy(@Param("userId") Long id); + + + /** + * 用户总 退款 + * + * @param id + * @return + */ + @Select("SELECT IFNULL(SUM(buff),0) FROM apple_refund_record WHERE user_id = #{userId}") + Integer countTotalRefund(@Param("userId") Long id); + + /** + * 查询用户 的 下单数量 + * + * @param id + * @return + */ + @Select("SELECT t.price_real FROM t_order t JOIN t_order_item t1 ON t1.order_id = t.id WHERE t.is_delete=0 AND t.buyer_id = #{userId} AND t1.source_type != 'HANDSEL' ") + List countOrder(@Param("userId") Long id); + + + + @Select("SELECT t.create_time FROM t_order t JOIN t_order_item t1 ON t1.order_id = t.id WHERE t.is_delete=0 AND t.buyer_id = #{userId} AND t1.source_type != 'HANDSEL' order by t.create_time asc limit 1 ") + Date getFirstOrderTime(@Param("userId") Long id); + + /** + * 查询有赠送 VIP 时间 的人 + * + * @return + */ + @Select(" SELECT user_id,member_type,reward_member_type ,reward_end_time ,end_time FROM t_user_member WHERE reward_end_time > NOW()") + List rewardEndTimeUsers(); + + + /** + * 查询没有 赠送 VIP 时间的人 + * + * @return + */ + @Select(" SELECT user_id,member_type,reward_member_type ,reward_end_time ,end_time FROM t_user_member WHERE (reward_end_time is null or reward_end_time NOW()") + List noRewardEndTimeUsers(); + + @Update("update t_pay_trade set channel_id = #{channel} where trade_no = #{tradeNo}") + int updateChannelByTradeNo(@Param("channel") String channel, @Param("tradeNo") String tradeNo); + + + /** + * 查询是否已经充值过 + * + * @param uid + * @return + */ + @Select("SELECT COUNT(1) FROM t_pay_trade WHERE src_account_id = #{uid} AND biz_type = 600 AND status IN (2,4,6,7) AND payment_trade_no is null") + int countAnyChargeOrders(@Param("uid") Long uid); + + @Update("update t_pay_trade set gift_amount = #{giftAmount} where id=#{id}") + int updateGiftAmountByTradeNo(@Param("id") Long id,@Param("giftAmount") Long giftAmount); +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingChargeDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingChargeDao.java new file mode 100644 index 0000000..1b182e0 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingChargeDao.java @@ -0,0 +1,23 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.ProcessingCharge; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +public interface ProcessingChargeDao extends BaseMapper { + + @Select("select * from t_processing_charge") + List findAll(); + + @Select("select * from t_processing_charge where pay_channel != 12") + List findOtherChannelAll(); + + @Select("select * from t_processing_charge where pay_channel = 12 and next_hand_time < now() limit 50") + List findPayssionChannelAll(); + + @Select("select * from t_processing_charge where call_channel_record_id = #{id} limit 1") + ProcessingCharge findByRecordId(@Param("id")Long id); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingDisputeDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingDisputeDao.java new file mode 100644 index 0000000..296ce81 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingDisputeDao.java @@ -0,0 +1,13 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.ProcessingDispute; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +public interface ProcessingDisputeDao extends BaseMapper { + + @Select("select * from t_processing_dispute") + List findAll(); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingWithdrawDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingWithdrawDao.java new file mode 100644 index 0000000..f4bb4e4 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingWithdrawDao.java @@ -0,0 +1,18 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.ProcessingWithdraw; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +public interface ProcessingWithdrawDao extends BaseMapper { + + @Select("select * from t_processing_withdraw where next_hand_time < now() limit 50") + List findAll(); + + + @Select("select * from t_processing_withdraw where call_channel_record_id = #{id} limit 1") + ProcessingWithdraw findByRecordId(@Param("id")Long id); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingWithdrawReviewDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingWithdrawReviewDao.java new file mode 100644 index 0000000..069767d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/ProcessingWithdrawReviewDao.java @@ -0,0 +1,15 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.ProcessingWithdrawReview; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; +import java.util.List; + +public interface ProcessingWithdrawReviewDao extends BaseMapper { + + @Select("select * from t_processing_withdraw_review where create_time < #{date}") + List findAll(@Param("date")LocalDateTime date); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/UserSubscribeLogDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/UserSubscribeLogDao.java new file mode 100644 index 0000000..c93de53 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/UserSubscribeLogDao.java @@ -0,0 +1,7 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.UserSubscribeLog; + +public interface UserSubscribeLogDao extends BaseMapper { +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/UserSubscriptionDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/UserSubscriptionDao.java new file mode 100644 index 0000000..19c8a48 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/UserSubscriptionDao.java @@ -0,0 +1,110 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.UserSubscription; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.time.LocalDateTime; +import java.util.List; + +public interface UserSubscriptionDao extends BaseMapper { + + /** + * 根据订阅ID 和 用户ID 得到 当前订阅. + * + * @param subscriptionId + * @param userId + * @return + */ + @Select("select * from user_subscription where subscription_id =#{subscriptionId} AND user_id =#{userId} limit 1") + UserSubscription getBySubscriptonIdAndUserId(@Param("subscriptionId") String subscriptionId, @Param("userId") Long userId); + + /** + * 根据用户查询订阅 + * + * @param userId + * @return + */ + @Select("select * from user_subscription where user_id =#{userId} order by edit_time desc limit 1") + UserSubscription getByUserId(@Param("userId") Long userId); + +// @Select("delete from user_subscription where exp_time < DATE_ADD(NOW() ,INTERVAL -31 DAY) ") +// UserSubscription clearExpTimeData(); + + /** + * 查询已经过期一天的数据 + * + * @return + */ + @Select("select * from user_subscription where exp_time < DATE_ADD(NOW() ,INTERVAL -1 DAY) and remind != 1") + List getExpiredSubscriptions(); + + + /** + * 查询已经过期了 的 订阅 + * + * @param platform + * @return + */ + @Select("select * from user_subscription where platform=#{platform} AND exp_time > #{beginTime} AND exp_time < #{endTime} ") + List getPaypalExpiredSubscriptions(@Param("platform") String platform, @Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); + + /** + * 查询最近的 订阅 + * + * @param subscriptionId + * @return + */ + @Select("select * from user_subscription where subscription_id =#{subscriptionId} order by edit_time desc limit 1") + UserSubscription getBySubscriptionId(@Param("subscriptionId") String subscriptionId); + + @Update("update user_subscription set purchase_token =#{purchaseToken} where id =#{id} ") + int updateToken(@Param("id") Long id, @Param("purchaseToken") String purchaseToken); + + + @Update("update user_subscription set remind = 1 where id =#{id} ") + void updateRemind(@Param("id") Long id); + + + @Update("update user_subscription set exp_time =#{exptime} where user_id =#{userId}") + int updateExpTimeByUserId(@Param("userId")Long userId, @Param("exptime")LocalDateTime exptime); + +// +// @Update("update user_subscription set purchase_time =#{purchaseTime} where id =#{id} ") +// int updatePurchaseTimeById(@Param("id")Long id, @Param("purchaseTime")LocalDateTime purchaseTime); +// +// +// @Update("update user_subscription set exp_time =#{exptime}, subscription_id =#{subscriptionId} , product_id =#{productId} ,member_type=#{memberType} where id =#{id} ") +// int updateProduct(@Param("id")Long id,@Param("subscriptionId") String subscriptionId, @Param("exptime")LocalDateTime exptime, @Param("productId")String productId, @Param("memberType")String memberType); +// +// @Update("update user_subscription set price_type =#{priceType} where id =#{id} ") +// int updatePriceType(@Param("id")Long id,@Param("priceType") String priceType ); +// +// @Update("update user_subscription set refund_time_ms =#{refundTimeMs}, purchase_token = #{purchaseToken}, subscription_id =#{subscriptionId} , product_id =#{productId} ,member_type=#{memberType} where id =#{id} ") +// int updateWhenPaypalUpgrade(@Param("id")Long id,@Param("subscriptionId") String subscriptionId,@Param("purchaseToken") String purchaseToken, @Param("refundTimeMs")Long refundTimeMs, @Param("productId")String productId, @Param("memberType")String memberType); +// +// @Update("update user_subscription set ip = #{ip} where id =#{id} ") +// int updateIp(@Param("id")Long id,@Param("ip") String ip); + + + List queryUserSubscription(@Param("userIdList") List userIdList); + + /** + * 查询未过期的订阅列表 + * + * @return + */ + @Select("select user_id from user_subscription where exp_time > now()") + List getNotExpiredSubscriptionList(); + + /** + * 更新自动续订状态 + * @param id + * @param autoRenew + * @return + */ + @Update("update user_subscription set auto_renew_status = #{autoRenew} where id = #{id} and auto_renew_status != #{autoRenew}") + int updateAutoRenew(@Param("id")Long id, @Param("autoRenew")Boolean autoRenew); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/UserSubscriptionNotifyDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/UserSubscriptionNotifyDao.java new file mode 100644 index 0000000..a063d5a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/UserSubscriptionNotifyDao.java @@ -0,0 +1,26 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.UserSubscriptionNotify; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +public interface UserSubscriptionNotifyDao extends BaseMapper { + + @Update("update user_subscription_notify set status = #{status} where id =#{id} ") + int updateStatus(@Param("id") Long id, @Param("status") String status); + + @Update("update user_subscription_notify set status = #{status}, subscription_id = #{subscriptionId} where id =#{id} ") + int updateStatusAndSubscriptionId(@Param("id") Long id, @Param("status") String status, @Param("subscriptionId") String subscriptionId); + + @Update("update user_subscription_notify set subscription_id = #{subscriptionId} where id =#{id} ") + int setSubscriptionId(@Param("id") Long id, @Param("subscriptionId") String subscriptionId); + + + @Update("update user_subscription_notify set extend = #{extend} where id =#{id} ") + int updateNotifyExtend(@Param("id") Long id, @Param("extend") String extend); + + + @Update("update user_subscription_notify set apple_refund_record_id = #{appleRefundRecordId}, extend = #{extend} where id =#{id} ") + int updateNotifyExtend(@Param("id") Long id, @Param("appleRefundRecordId") Long appleRefundRecordId, @Param("extend") String extend); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/dao/WithdrawRequestDao.java b/sonic-lion/server/src/main/java/com/sonic/lion/dao/WithdrawRequestDao.java new file mode 100644 index 0000000..476604e --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/dao/WithdrawRequestDao.java @@ -0,0 +1,30 @@ +package com.sonic.lion.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.lion.domain.entity.WithdrawRequest; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.time.LocalDateTime; +import java.util.List; + +public interface WithdrawRequestDao extends BaseMapper { + + // 这里都是换算成buff的 + @Select("select IFNULL(SUM(amount),0) from t_withdraw_request where src_account_id = #{uid} AND status=0 AND coin_type = #{coinType}") + int totalRequest(@Param("uid") Long srcAccountId, @Param("coinType") String coinType); + + @Select("select count(1) from account_buff where withdrawable_income >= #{amount} AND uid = #{uid}") + int checkBuffWithdrawAble(@Param("uid") Long srcAccountId, @Param("amount") Long amount); + + @Select("select count(1) from account_ecoin where withdrawable_income >= #{amount} AND uid = #{uid}") + int checkEcoinWithdrawAble(@Param("uid") Long srcAccountId, @Param("amount") Long amount); + + @Select("select * from t_withdraw_request where status =0 AND create_time <= #{createTime} ") + List getUnPaidRequest(@Param("createTime") LocalDateTime createTime); + + @Update("update t_withdraw_request set status = 1 where id=#{id}") + int updateStatus(@Param("id") Long id); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/MemberPrivDict.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/MemberPrivDict.java new file mode 100644 index 0000000..e24f6fd --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/MemberPrivDict.java @@ -0,0 +1,62 @@ +package com.sonic.lion.domain; + +import com.baomidou.mybatisplus.annotation.*; +import com.sonic.lion.enums.MemberPrivEnum; +import lombok.Data; +import java.sql.Timestamp; + +/** + * 会员特权字典表 + */ +@Data +@TableName("member_priv_dict") +public class MemberPrivDict { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 会员特权code + */ + @TableField("code") + private MemberPrivEnum code; + + /** + * 会员特权标题 + */ + @TableField("title") + private String title; + + /** + * 会员特权描述 + */ + @TableField("`desc`") + private String desc; + + /** + * 图片 + */ + @TableField("img") + private String img; + + /** + * 排序 + */ + @TableField("sort") + private Integer sort; + + /** + * 是否删除 0:未删除 1:删除 + */ + @TableField("is_delete") + private Integer isDelete; + + /** + * 创建时间 + */ + @TableField("create_time") + private Timestamp createTime; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/SubscriptionBo.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/SubscriptionBo.java new file mode 100644 index 0000000..6120888 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/SubscriptionBo.java @@ -0,0 +1,39 @@ +package com.sonic.lion.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 订阅数据 + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class SubscriptionBo { + + private String id; + + private String customer; + + private Long currentPeriodStart; + + private Long currentPeriodEnd; + + /** + * 是否自动续订(true 是、false 否) + */ + private Boolean cancelAtPeriodEnd; + + /** + * 订阅状态 + */ + private String status; + + private String priceId; + + + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/TradeStatusBo.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/TradeStatusBo.java new file mode 100644 index 0000000..b06f34d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/TradeStatusBo.java @@ -0,0 +1,27 @@ +package com.sonic.lion.domain.bo; + +import com.sonic.lion.enums.TradeStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @Author code + * @Description 交易状态BO对象 + * @Date 2023/12/15 20:13 + * @Version 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TradeStatusBo { + + private TradeStatus tradeStatus; + + private List billIdList; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/WebhookBo.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/WebhookBo.java new file mode 100644 index 0000000..dabb007 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/WebhookBo.java @@ -0,0 +1,26 @@ +package com.sonic.lion.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author zzhan + * @Description TODO + * @Date 2023/5/15 10:55 + * @Version 1.0 + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class WebhookBo { + + private String id; + + private String eventType; + + private String customerId; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/WithdrawTradeExtendBo.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/WithdrawTradeExtendBo.java new file mode 100644 index 0000000..e08e20d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/bo/WithdrawTradeExtendBo.java @@ -0,0 +1,30 @@ +package com.sonic.lion.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author code + * @Description 提现交易表扩展字段的扩展值 + * @Date 2024/9/28 11:42 + * @Version 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WithdrawTradeExtendBo { + + /** + * 如果免手续费 , 免手续费的流水id + */ + private Long freeWithdrawBillId; + + /** + * 提现的货币类型 + */ + private String currencyType; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AccountBuff.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AccountBuff.java new file mode 100644 index 0000000..9af5f70 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AccountBuff.java @@ -0,0 +1,101 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * account_buff + * + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "account_buff", autoResultMap = true) +public class AccountBuff implements Serializable { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户id + */ + private Long uid; + + /** + * 可消费金额 + */ + private Long balance; + + /** + * 可提现收入 + */ + private Long withdrawableIncome; + + /** + * 待入账收入 + */ + private Long awaitingIncome; + + /** + * 冻结收入 + */ + private Long frozenIncome; + + /** + * 冻结余额 + */ + private Long frozenBalance; + + /** + * 总充值 + */ + private long rechargeTotal; + + /** + * 在途的 提现金额 + */ + private Long withdrawOnGoing; + + private Status status; + + private LocalDateTime createTime; + + private LocalDateTime editTime; + + private static final long serialVersionUID = 1L; + + @Getter + public enum Status { + + ENABLE(0, "ENABLE"), + + DISABLE(1, "DISABLE"), + ; + + private final int value; + + private final String desc; + + Status(int value, String desc) { + this.value = value; + this.desc = desc; + } + } + + /** + * 获取总金额 + * + * @return + */ + public long getTotalAmount() { + return this.getBalance() + this.getWithdrawableIncome() + this.getAwaitingIncome() + this.getFrozenIncome(); + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AccountBuffAwaiting.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AccountBuffAwaiting.java new file mode 100644 index 0000000..7c9ab13 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AccountBuffAwaiting.java @@ -0,0 +1,88 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * account_buff_awaiting + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "account_buff_awaiting", autoResultMap = true) +public class AccountBuffAwaiting implements Serializable { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 流水id account_buff_bill 主键id + */ + private Long billId; + + /** + * account_buff.id + */ + private Long accountId; + + private String tradeNo; + + private Long buff; + + /** + * 转入可提现收入时间 + */ + private LocalDateTime toWithdrawableIncomeTime; + + private Status status; + + private FronzenStatus fronzenStatus; + + private LocalDateTime createTime; + + private LocalDateTime editTime; + + + private static final long serialVersionUID = 1L; + + public enum FronzenStatus { + UN_FRONZEN , FRONZEN + } + + @Getter + public enum Status { + + /** + * 待入账 + */ + PENDING(0, "PENDING"), + + /** + * 已入账 + */ + CREDITED(1, "CREDITED"), + + /** + * 已退款 + */ + REFUNDED(2, "REFUNDED"), + ; + + private final int value; + + private final String desc; + + Status(int value, String desc) { + this.value = value; + this.desc = desc; + } + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AccountBuffBill.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AccountBuffBill.java new file mode 100644 index 0000000..73f6e96 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AccountBuffBill.java @@ -0,0 +1,155 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.domain.enums.*; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.I18nResources; +import lombok.*; +import org.apache.commons.lang3.StringUtils; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * account_buff_bill + * + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "account_buff_bill", autoResultMap = true) +public class AccountBuffBill implements Serializable { + + @TableId(type = IdType.AUTO) + private Long id; + + private String platform; + + private String billNo; + + private Long accountId; + + private Long uid; + + private Long desUid; + + /** + * 游戏订单 担保交易 冗余目标方的用户id + */ + private Long targetUserId; + + private Long buff; + + /** + * Buff 归类 + */ + private BuffClassifyEnum buffClassify; + + /** + * 赠送金额 + */ + private Long giftAmount; + + private BizType bizType; + + private PayChannel payChannel; + + private String tradeNo; + + private String bizNo; + + private String payMethod; + + /** + * 外部交易号关联单号(如:打赏单号关联的游戏订单的订单号) + */ + private String bizNoRelationNo; + + private Long balance; + + private InOrOut inOrOut; + + private BuffType buffType; + + /** + * 转入可提现收入时间 + */ + private LocalDateTime toWithdrawableIncomeTime; + + private LocalDateTime createTime; + + private LocalDateTime editTime; + + + private BillStatusEnum billStatus; + + /** + * 提现状态 + */ + private WithdrawStatus withdrawStatus; + + private String reason; + + private String extend; + + @TableLogic + @TableField("is_deleted") + private Boolean deleted; + + private static final long serialVersionUID = 1L; + + /** + * 提现状态 + */ + @AllArgsConstructor + @Getter + public enum WithdrawStatus { + //审核中 + IN_REVIEW("review"), + //审核失败 + REVIEW_FAIL("review failed"), + //提现中 + WITHDRAW_ING("processing"), + //提现失败 + WITHDRAW_FAIL("failed"), + WITHDRAW_SUCCESS("success"), + //提现失败返还 + WITHDRAW_FAIL_BACK("failed back"), + ; + private String status; + + } + + public String getStatus() { + if (BuffType.AWAITING_INCOME.equals(buffType) && InOrOut.IN.equals(inOrOut)) { + //为待入帐状态 + return I18nResources.AWAITING_INCOME_STATUS.getI18n(); + } + if (BizType.WITHDRAW.equals(bizType) && withdrawStatus != null && !WithdrawStatus.WITHDRAW_FAIL_BACK.equals(withdrawStatus)) { + return I18nResources.valueOf(withdrawStatus.name()).getI18n(); + } + return I18nResources.WITHDRAW_SUCCESS.getI18n(); + } + + + /** + * 前端界面 优先显示 支付方式. + * + * @return + */ + public String getPayment() { + if (StringUtils.isNotBlank(payMethod)) { + if ("sofort".equals(payMethod)) { + return "Sofort"; + } + if ("giropay_de".equals(payMethod)) { + return "Giropay"; + } + return payMethod; + } + return payChannel.getDesc(); + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AppStoreProduct.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AppStoreProduct.java new file mode 100644 index 0000000..fd5bc53 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AppStoreProduct.java @@ -0,0 +1,97 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.domain.enums.ProductType; +import com.sonic.lion.enums.MemberType; +import lombok.*; + +import java.math.BigDecimal; +import java.util.Date; +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "t_app_product", autoResultMap = true) +public class AppStoreProduct { + + @TableId(type = IdType.AUTO) + private Long id; + + private String platform; + + /** + * 产品类型 连续购买订阅 和 非连续 + */ + private ProductType productType; + + /** + * 订阅套餐类型 + */ + private MemberType memberType; + + private PERIOD period; + + /** + * 应用id + */ + private String bundleId; + + /** + * 内购商品项id + */ + private String productId; + + /** + * 优惠多少 百分比 + */ + private String discount; + + /** + * 得到的Buff + */ + private Long chargeAmount; + + /** + * 支付金额 + */ + private Long payAmount; + + private Date createTime; + + private Date editTime; + + @TableField("is_delete") + private Boolean deleted; + + private String version; + + @Getter + public enum PERIOD{ + SUB_MONTH(30*86400 ,1 ),SUB_SEASON(90*86400, 3),SUB_YEAR(360*86400, 12); + int seconds; + int month; + PERIOD(int value,int month){ + this.seconds = value; + this.month = month; + } + } + + /** + * 折合每月多少 + */ + public Long getMonthlyPrice(){ + if(PERIOD.SUB_MONTH.equals(period)){ + return payAmount; + } + if(PERIOD.SUB_SEASON.equals(period)){ + return new BigDecimal(payAmount) .divide(new BigDecimal("3"), 0, BigDecimal.ROUND_HALF_UP).longValue(); + } + if(PERIOD.SUB_YEAR.equals(period)){ + return new BigDecimal(payAmount) .divide(new BigDecimal("12"), 0, BigDecimal.ROUND_HALF_UP).longValue(); + } + return 0L; + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AppleRefundRecord.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AppleRefundRecord.java new file mode 100644 index 0000000..197a20d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/AppleRefundRecord.java @@ -0,0 +1,127 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.BizType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "apple_refund_record", autoResultMap = true) +public class AppleRefundRecord { + + @TableId(type = IdType.AUTO) + private Long id; + /** + * 产品ID + */ + private String productId; + + /** + * 业务类型 + */ + private BizType bizType; + /** + * 交易编号(第三方) + */ + private String transactionId; + + /** + * 争议Id + */ + private String disputeId; + /** + * 购买时间 + */ + private LocalDateTime purchaseDate; + /** + * 退款时间 + */ + private LocalDateTime cancellationDate; + /** + * 交易单号 + */ + private String tradeNo; + /** + * 用户ID + */ + private Long userId; + /** + * 购买数量 + */ + private Long buff; + /** + * 购买金额 + */ + private Long amount; + + /** + * 平台 + */ + private Platform platform; + + + /** + * 第三方接口的原始内容 + */ + private String content; + //冻结状态 扣减(还有余额) 冻结(没有余额 ) + private FronzenStatus fronzenStatus; + + //冻结之后 的金额 + private Long afterFronzenBuff; + + + public enum FronzenStatus { + INIT, DEC, FRONZEN + } + + public enum Platform { + PAYPAL, IOS, GOOGLE, PAYMENTWALL, CHECKOUT, AIRWALLEX; + } + + + private String createTime; + private String transactionTime; + private String transactionStatus; + @TableField(exist = false) + private String currency="USD"; + private String buyerName; + private String reason; + private String status; + + @TableField(exist = false) + private String sellerResponseDueDate; + private String ip; + private String email; + private String nickname; + private String idCard; + private LocalDateTime registerTime; + private LocalDateTime pstPayTime; + + /** + * [{"kind":"androidpublisher#voidedPurchase","purchaseTimeMillis":"1635348172211", + * * "purchaseToken":"doibnmclnepnigcblgnodkgo.AO-J1OyK0N8BkSeDyyMpQeOG-Z7jcadCTMxkINqXVPZWcpl91YOqVkaOkWS4OqGl3WHk1XAXwx6P8DNQwMxLrmxXLGnM0ULr7g", + * * "voidedTimeMillis":"1636432172312","orderId":"GPA.3327-1510-4846-05509","voidedSource":0,"voidedReason":7}] + * + */ + + /** + * 订单ID 购买商品名称 购买数量 购买金额 购买时间 退款时间 用户ID 订单号 客户端(GP/苹果/web) + */ + + + /** + * 数据插入时间 + */ + private LocalDateTime insertTime; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/BuffRewardRecord.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/BuffRewardRecord.java new file mode 100644 index 0000000..d17adee --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/BuffRewardRecord.java @@ -0,0 +1,67 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +@TableName("buff_reward_record") +public class BuffRewardRecord { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户Id + */ + private Long uid; + + /** + * 金额 + */ + private Long amount; + + /** + * 奖励原因 + */ + private String rewardType; + + /** + * 充值的档位 + */ + private String productId; + + + /** + * 奖品的档位Id + */ + private Long rewardId; + + /** + * 数字年月(yyyyMM) + */ + private Integer yearMonthInt; + + /** + * PST 时间 + * 领取日期 (当天只能领取一次) + */ + private LocalDate rewardDate; + + + /** + * 创建时间 + */ + private LocalDateTime createTime; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ChannelBlacklist.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ChannelBlacklist.java new file mode 100644 index 0000000..4ba4b5b --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ChannelBlacklist.java @@ -0,0 +1,54 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("channel_blacklist") +public class ChannelBlacklist { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 渠道类型 + * PayChannel 枚举 + */ + private String channelType; + + /** + * 拉黑次数 + */ + private Integer blockCount; + + /** + * 是否删除 + */ + private Boolean isDelete; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + private LocalDateTime editTime; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/FreeWithdrawConfig.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/FreeWithdrawConfig.java new file mode 100644 index 0000000..f7c3d9c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/FreeWithdrawConfig.java @@ -0,0 +1,62 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.FreeWithdrawReason; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * account_buff + * + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "t_free_withdraw_config", autoResultMap = true) +public class FreeWithdrawConfig implements Serializable { + + @TableId(type = IdType.AUTO) + private Long id; + /** + * 用户ID + */ + private Long uid; + /** + * 开始时间 + */ + private LocalDateTime startTime; + + /** + * 结束时间 + */ + private LocalDateTime endTime; + + /** + * 提现手续费减免比例 + */ + private BigDecimal rate; + + /** + * 获得原因 + */ + private FreeWithdrawReason reason; + + private LocalDateTime createTime; + + private LocalDateTime editTime; + + private Boolean isDelete; + + private static final long serialVersionUID = 1L; + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/FreeWithdrawFeeBill.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/FreeWithdrawFeeBill.java new file mode 100644 index 0000000..50a89b7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/FreeWithdrawFeeBill.java @@ -0,0 +1,62 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.FreeWithdrawReason; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @author code + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "t_free_withdraw_bill", autoResultMap = true) +public class FreeWithdrawFeeBill { + /** + * id + */ + @TableId(type = IdType.AUTO) + private Long id; + /** + * 用户Id + */ + private Long uid; + /** + * 相关配置Id + */ + private Long freeConfigId; + + /** + * 交易编号 + */ + private String tradeNo; + /** + * 原因 + */ + private FreeWithdrawReason reason; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 最后修改时间 + */ + private LocalDateTime editTime; + + /** + * 是否删除 + */ + @TableField("is_delete") + private Boolean deleted; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/GoogleRecord.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/GoogleRecord.java new file mode 100644 index 0000000..29c7044 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/GoogleRecord.java @@ -0,0 +1,53 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * @author: code + * @date: 2025/07/15 + * @Description: Google内购已处理过交易的记录 + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "t_pay_google_record", autoResultMap = true) +public class GoogleRecord { + + @TableId(type = IdType.AUTO) + private Long id; + + private String tradeNo; + + /** + * 苹果内购系统交易id + */ + private String transactionId; + + private Long receiptId; + + /** + * 应用名 + */ + private String bundleId; + + /** + * 内购项 + */ + private String productId; + + private String result; + + private Date createTime; + + private Date editTime; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/GoogleUploadReceipt.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/GoogleUploadReceipt.java new file mode 100644 index 0000000..6eb5f21 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/GoogleUploadReceipt.java @@ -0,0 +1,49 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @author: code + * @date: 2025/07/15 + * @Description: iOS上传的票据 + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "google_upload_receipt", autoResultMap = true) +public class GoogleUploadReceipt { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 本单收据所包含苹果交易号所对应的系统内部交易号,格式:{transactionId:tradeNo} + */ + private String transactionsJsonStr; + + /** + * 加密的收据 + */ + private String receipt; + + /** + * 是否已处理 0:否 1:是 + */ + @TableField("is_processed") + private Boolean processed; + + private LocalDateTime createTime; + + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/IapRecord.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/IapRecord.java new file mode 100644 index 0000000..c4b9302 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/IapRecord.java @@ -0,0 +1,53 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * @author: code + * @date: 2025/07/15 + * @Description: iOS内购已处理过交易的记录 + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "t_pay_iap_record", autoResultMap = true) +public class IapRecord { + + @TableId(type = IdType.AUTO) + private Long id; + + private String tradeNo; + + /** + * 苹果内购系统交易id + */ + private String transactionId; + + private Long receiptId; + + /** + * 应用名 + */ + private String bundleId; + + /** + * 内购项 + */ + private String productId; + + private String result; + + private Date createTime; + + private Date editTime; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/IapUploadReceipt.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/IapUploadReceipt.java new file mode 100644 index 0000000..b1b7808 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/IapUploadReceipt.java @@ -0,0 +1,65 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @author: code + * @date: 2025/07/15 + * @Description: iOS上传的票据 + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "iap_upload_receipt", autoResultMap = true) +public class IapUploadReceipt { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 本单收据所包含苹果交易号所对应的系统内部交易号,格式:{transactionId:tradeNo} + */ + private String transactionsJsonStr; + + /** + * 加密的收据 + */ + private String receipt; + + /** + * 用户ID + */ + private Long userId; + + /** + * 交易号 + */ + private String tradeNo; + + /** + * 是否已处理 0:否 1:是 + */ + @TableField("is_processed") + private Boolean processed; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 编辑时间 + */ + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountBill.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountBill.java new file mode 100644 index 0000000..6aac0f8 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountBill.java @@ -0,0 +1,135 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * t_pay_account_bill + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_pay_account_bill") +public class PayAccountBill { + /** + * ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 平台代码 + */ + private String platform; + + /** + * 用户流水ID + */ + private String accountBillId; + + /** + * 流水所属平台账号id + */ + private Long accountId; + + /** + * 账号名称 + */ + private String accountName; + + /** + * 交易号 + */ + private String tradeNo; + + /** + * 通道账号 (如:银行卡号,支付宝账号;根据des_account_type确定) + */ + private String desAccountNo; + + /** + * 通道账号姓名 + */ + private String desAccountName; + + /** + * 通道账号类型:0平台账号,1非平台账号 + */ + private Integer desAccountType; + + /** + * 渠道ID + */ + private Integer channelId; + + /** + * 渠道名称 + */ + private String channelName; + + /** + * 业务类型(参看枚举TradeBizType) + */ + private Integer bizType; + + /** + * 业务编号ID(参看枚举TradeBizType) + */ + private String bizNum; + + /** + * 支付方式ID + */ + private Long paymentTypeId; + + /** + * 流水类型:1收入,2支出 + */ + private Integer inOrOut; + + /** + * 流水标题或名称 + */ + private String name; + + /** + * 发生金额(分) + */ + private Long amount; + + /** + * 账户当前余额(分),status=2成功时更新 + */ + private Long balance; + + /** + * 备注 + */ + private String remark; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 最后修改时间 + */ + private LocalDateTime editTime; + + /** + * 是否删除 + */ + @TableField("is_delete") + private Boolean deleted; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFund.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFund.java new file mode 100644 index 0000000..136a9d5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFund.java @@ -0,0 +1,88 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * t_pay_account_fund + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_pay_account_fund") +public class PayAccountFund { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 账号id + */ + private Long accountId; + + /** + * 支付渠道 Id(2:PayPal, 3:stripe, 默认为 2) + */ + private Integer channelId; + + /** + * 帐户状态 1正常 2冻结 + */ + private Integer status; + + /** + * 余额(可提现) + */ + private Long balance; + + /** + * 充值金额(可消费) + */ + private Long charge; + + /** + * 已冻结的额度 + */ + private Long frozenAmount; + + /** + * 待入账金额 + */ + private Long awaitingAmount; + + /** + * 日切可交易额度 + */ + private Long dailyBalance; + + /** + * 日切已冻结额度 + */ + private Long dailyFrozenAmount; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 最后修改时间 + */ + private Date editTime; + + /** + * 是否删除 + */ + @TableField("is_delete") + private Boolean deleted; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFundAwaiting.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFundAwaiting.java new file mode 100644 index 0000000..b570f5c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFundAwaiting.java @@ -0,0 +1,75 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * t_pay_account_fund_awaiting + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "t_pay_account_fund_awaiting", autoResultMap = true) +public class PayAccountFundAwaiting { + /** + * ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 账号id + */ + private Long accountId; + + /** + * 帐户状态 0未入账 1已入账 + */ + private Boolean status; + + /** + * 待入账金额 + */ + private Long awaitingAmount; + + /** + * 交易号 + */ + private String tradeNo; + + /** + * 支付渠道 Id(2:PayPal, 3:stripe, 默认为 2) + */ + private Integer channelId; + + /** + * 入账时间 + */ + private LocalDateTime toBalanceTime; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 最后修改时间 + */ + private LocalDateTime editTime; + + /** + * 是否删除 + */ + @TableField("is_delete") + private Boolean deleted; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFundFrozen.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFundFrozen.java new file mode 100644 index 0000000..0422ae3 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFundFrozen.java @@ -0,0 +1,125 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * t_pay_account_fund_frozen + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_pay_account_fund_frozen") +public class PayAccountFundFrozen { + /** + * 资金冻结记录id + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 平台代码 + */ + private String platform; + + /** + * 帐户的id + */ + private Long accountId; + + /** + * 账号名称(冗余) + */ + private String accountName; + + /** + * 关联渠道表流水ID + */ + private String channelBillId; + + /** + * 交易号 + */ + private String tradeNo; + + /** + * 冻结解冻的金额 + */ + private Long amount; + + /** + * 冻结状态:1冻结 2解冻 + */ + private Integer freezeStatus; + + /** + * 冻结时间 + */ + private LocalDateTime freezeDate; + + /** + * 解冻时间 + */ + private LocalDateTime unfreezeDate; + + /** + * 冻结类型 + */ + private Integer freezeType; + + /** + * 资金账户id + */ + private Long accountFundId; + + /** + * 冻结人id + */ + private Long freezeUserId; + + /** + * 冻结人名字 + */ + private String freezeUserName; + + /** + * 解冻人ID + */ + private Long unfreezeUserId; + + /** + * 解冻人名字 + */ + private String unfreezeUserName; + + /** + * 备注 + */ + private String remark; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 最后修改时间 + */ + private LocalDateTime editTime; + + /** + * 是否删除 + */ + @TableField("is_delete") + private Boolean deleted; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFundThird.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFundThird.java new file mode 100644 index 0000000..cb9df8a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountFundThird.java @@ -0,0 +1,93 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.domain.enums.UserThirdStatus; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.ThirdAccountType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * t_pay_account_fund_third + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_pay_account_fund_third") +public class PayAccountFundThird { + /** + * ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 账号id + */ + private Long accountId; + + /** + * 资金帐号ID + */ + private Long accountFundId; + + /** + * 第三方类型 + */ + private ThirdAccountType appType; + + /** + * 第三方关联ID + */ + private String openId; + + /** + * 扩展信息 + */ + private String extend; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 最后修改时间 + */ + private Date editTime; + + /** + * 是否删除 + */ + @TableField("is_delete") + private Boolean deleted; + + /** + * 三方账户邮箱 + */ + private String email; + + /** + * 三方账户名 + */ + private String name; + + /** + * 渠道id + */ + private PayChannel channelId; + + /** + * 状态 + */ + private UserThirdStatus status; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountThirdBind.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountThirdBind.java new file mode 100644 index 0000000..2a68aa3 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayAccountThirdBind.java @@ -0,0 +1,54 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.BindDataType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * t_pay_account_third_bind + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_pay_account_third_bind") +public class PayAccountThirdBind { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long accountThirdId; + + /** + * 绑定资料类型 + */ + private BindDataType bindDataType; + + /** + * 绑定资料id(对应各资料表id) + */ + private Long bindDataId; + + /** + * 资料在三方的openId + */ + private String thirdOpenId; + + private String result; + + private Date createTime; + + private Date editTime; + + @TableField("is_delete") + private Boolean deleted; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayCallChannelRecord.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayCallChannelRecord.java new file mode 100644 index 0000000..25de0d8 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayCallChannelRecord.java @@ -0,0 +1,128 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.domain.enums.CallChannelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * t_pay_call_channel_record + * + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_pay_call_channel_record") +public class PayCallChannelRecord { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 交易单号 + */ + private String tradeNo; + + /** + * 业务类型 + */ + private BizType bizType; + + /** + * 支付渠道 + */ + private PayChannel channel; + + /** + * 调用渠道状态 + */ + private CallChannelStatus status; + + /** + * 是否发生了争议 + */ + private Boolean disputed; + + /** + * 争议发生时间 + */ + private LocalDateTime disputedTime; + + /** + * 金额(单位:分) + */ + private Long amount; + + /** + * 换汇 货币 + */ + private String exchangeCurrency; + + /** + * 换汇 原始金额 + */ + private Long exchangeAmount; + + /** + * 支付渠道返回处理号 + */ + private String batchId; + + /** + * 交易成功后渠道返回交易号 + */ + private String transactionId; + + /** + * web支付url + */ + private String paymentUrl; + + /** + * 渠道返回交易号 + */ + private String result; + + /** + * 最后一次查询时间 + */ + private LocalDateTime lastCheckTime; + + /** + * 下一次查询时间 + */ + private LocalDateTime nextCheckTime; + + /** + * 查询次数 + */ + private Integer checkNum; + + private LocalDateTime createTime; + + private LocalDateTime editTime; + + + private String payerId; + + private String payerName; + + private String payerEmail; + + + /** + * 渠道检查的过期时间 + */ + private LocalDateTime expTime; + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayChannelBill.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayChannelBill.java new file mode 100644 index 0000000..81c0d88 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayChannelBill.java @@ -0,0 +1,174 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.domain.enums.InOrOut; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * t_pay_channel_bill + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_pay_channel_bill") +public class PayChannelBill { + + /** + * ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 平台代码 + */ + private String platform; + + /** + * 渠道流水ID + */ + private String channelBillId; + + /** + * 流水所属平台账号id + */ + private Long accountId; + + /** + * 账号名称(冗余) + */ + private String accountName; + + /** + * 渠道流水状态:参看BillStatus + */ + private Integer status; + /** + * 状态说明;如失败和未知原因 + */ + private String statusMsg; + + /** + * 渠道ID + */ + @TableField("channel_id") + private PayChannel payChannel; + + /** + * 渠道名称(冗余) + */ + private String channelName; + + /** + * 渠道提交编号(通道内唯一;如通道没有特别要求默认与主键相同) + */ + private String submitId; + + /** + * 渠道流水号(通道返回) + */ + private String channelSn; + + /** + * 渠道返回的对账日期 + */ + private LocalDateTime channelSettleDate; + + /** + * 通道账号 (如:银行卡号,支付宝账号,资金账号;根据des_account_type确定) + */ + private String desAccountNo; + + /** + * 通道账号姓名 + */ + private String desAccountName; + + /** + * 通道账号类型:0平台账号,1非平台账号 + */ + private Integer desAccountType; + + /** + * 支付方式ID + */ + private Long paymentTypeId; + + /** + * 业务类型(参看枚举TradeBizType) + */ + private BizType bizType; + + /** + * 业务编号(参看枚举TradeBizType) + */ + private String bizNum; + + /** + * 流水类型:1收入,2支出 + */ + private InOrOut inOrOut; + + /** + * 流水标题或名称 + */ + private String name; + + /** + * 发生金额(分) + */ + private Long amount; + + /** + * 实际交易金额(分) + */ + private Long occurAmount; + + /** + * 手续费 + */ + private Long fee; + + /** + * 记账日(日切相关,对账时使用) + */ + private LocalDateTime tradeDate; + + /** + * 备注 + */ + private String remark; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 最后修改时间 + */ + private Date editTime; + + /** + * 是否删除 + */ + private Integer isDelete; + + /** + * 交易确认流水号(通道返回,退款需要使用) + */ + private String captureChannelSn; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayCharge.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayCharge.java new file mode 100644 index 0000000..0f6b0bb --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayCharge.java @@ -0,0 +1,62 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * t_pay_charge + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "t_pay_charge", autoResultMap = true) +public class PayCharge { + + @TableId(type = IdType.AUTO) + private Long id; + + private String platform; + + /** + * 应用id + */ + private String bundleId; + + /** + * 内购商品项id + */ + private String productId; + + private Long chargeAmount; + + private Long payAmount; + + /** + * 赠送金额 + */ + private Long giftAmount; + + /** + * 业务类型(HOT 热门标记、LARGE_PRODUCT 大额充送档位) + */ + private String bizType; + + private Date createTime; + + private Date editTime; + + @TableField("is_delete") + private Boolean deleted; + + private String version; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayConfig.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayConfig.java new file mode 100644 index 0000000..fdf6dee --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayConfig.java @@ -0,0 +1,41 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * t_pay_config + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_pay_config") +public class PayConfig { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("`key`") + private String key; + + private String value; + + /** + * 说明 + */ + @TableField("`desc`") + private String desc; + + private Date createTime; + + private Date editTime;} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayTrade.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayTrade.java new file mode 100644 index 0000000..9269389 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/PayTrade.java @@ -0,0 +1,293 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.domain.enums.CoinType; +import com.sonic.lion.enums.PaymentType; +import com.sonic.lion.enums.TradeStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * t_pay_trade + * + * @author + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "t_pay_trade", autoResultMap = true) +public class PayTrade { + /** + * ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 平台代码 + */ + private String platform; + + /** + * 交易号 + */ + private String tradeNo; + + /** + * 外部交易号(如订单号) + */ + private String outTradeNo; + + /** + * 外部交易号关联单号(如:打赏单号关联的游戏订单的订单号) + */ + private String outTradeNoRelationNo; + + /** + * 交易标题或名称 + */ + private String name; + + /** + * 本方(发起方)账户(account .id) + */ + private Long srcAccountId; + + /** + * 本方(发起方)账户名称 + */ + private String srcAccountName; + + /** + * 收款方账号编号 + */ + private String desAccountNo; + + /** + * 对方(目标)账号-名称或姓名 + */ + private String desAccountName; + + /** + * 收款方账号类型:参看DesAccountType + */ + private Integer desAccountType; + + /** + * 渠道ID + */ + @TableField("channel_id") + private PayChannel payChannel; + + /** + * 渠道名称(冗余) + */ + private String channelName; + + /** + * 渠道流水id + */ + private String channelBillId; + + /** + * 支付方式ID + */ + @TableField("payment_type_id") + private PaymentType paymentType; + + /** + * 交易业务分类(参看枚举TradeBizType) + */ + private BizType bizType; + + /** + * 充值时:用户实得金额 + */ + private Long actualAmount; + + /** + * 交易金额,单位:分 + */ + private Long amount; + + /** + * 交易实际成交金额,单位:分 + */ + private Long occurAmount; + + /** + * 优惠金额 + */ + private Long promoAmount; + + /** + * 手续费 + */ + private Long fee; + + + /** + * 充值的产品Id + */ + private String productId; + + /** + * 赠送金额 + */ + private Long giftAmount; + + /** + * 交易状态 1待付款 2已付款 3处理中 4交易成功 5交易关闭 6退款中 7已退款 + */ + private TradeStatus status; + + /** + * 付款时间 + */ + private LocalDateTime payTime; + + /** + * 交易结束时间 + */ + private LocalDateTime finishTime; + + /** + * 交易关闭时间druiddruid + */ + private LocalDateTime closeTime; + + /** + * 当前状态流入时间 + */ + private LocalDateTime statusInTime; + + /** + * 是否完成交易 + */ + @TableField("is_complete") + private Boolean complete; + + /** + * 备注 + */ + private String remark; + + /** + * 充值后支付交易号 + */ + private String paymentTradeNo; + + /** + * 资源key + */ + private String resourceKey; + + /** + * 资源数量 + */ + private Integer resourceNum; + + /** + * 交易异步通知url + */ + private String notifyUrl; + + /** + * 乐观锁 + */ + private Long version; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 最后修改时间 + */ + private LocalDateTime editTime; + + /** + * 是否删除 + */ + @TableField("is_delete") + private Boolean deleted; + + /** + * 扩展信息 + */ + private String extend; + + private String clientVersion; + + /** + * 提现币类型 + */ + private CoinType coinType; + + private String errorMessage; + + private String ip; + + /** + * 沙盒数据 0 否 1 是 + */ + private Integer standbox; + + private String evidence; + + + /** + * 平台费用 10% + */ + private Long platformFee; + + /** + * 三方费用 + */ + private Long thirdFee; + + + /** + * 支付方式 + */ + private String payMethod; + + /** + * 换汇发生金额 + */ + private Long exchangeOccurAmount; + + /** + * 换汇 货币 + */ + private String exchangeOccurCurrency; + + /** + * 汇率 1 USD 兑换 N Currency + */ + private Double exchangeRate; + + /** + * 换汇 原始金额 + */ + private Long exchangeProductAmount; + + /** + * 换汇手续费 + */ + private Long exchangeFee; + + + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingCharge.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingCharge.java new file mode 100644 index 0000000..b5d8842 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingCharge.java @@ -0,0 +1,55 @@ +package com.sonic.lion.domain.entity; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.PayChannel; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * @author code + */ +@ToString +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_processing_charge") +public class ProcessingCharge { + + @TableId(type = IdType.AUTO) + private Long id; + /** + * 渠道记录ID + */ + private Long callChannelRecordId; + + /** + * tradeNo + */ + private String tradeNo; + + /** + * 渠道类型 + */ + private PayChannel payChannel; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 下次处理时间 + */ + private LocalDateTime nextHandTime; + + /** + * 处理次数 + */ + private Integer handCount; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingDispute.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingDispute.java new file mode 100644 index 0000000..1da52e1 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingDispute.java @@ -0,0 +1,40 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; +@ToString +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_processing_dispute") +public class ProcessingDispute { + + @TableId(type = IdType.AUTO) + private Long id; + + private String disputeId; + + private String buyerTransactionId; + + private String sellerTransactionId; + + /** + * 0 待处理、2 处理失败 + */ + private Integer status; + + /** + * 异常内容 + */ + private String error; + + private LocalDateTime createTime; + + private LocalDateTime editTime; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingWithdraw.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingWithdraw.java new file mode 100644 index 0000000..e0c7d92 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingWithdraw.java @@ -0,0 +1,53 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.PayChannel; +import lombok.*; + +import java.time.LocalDateTime; + +@ToString +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_processing_withdraw") +public class ProcessingWithdraw { + + + @TableId(type = IdType.AUTO) + private Long id; + /** + * 渠道记录ID + */ + private Long callChannelRecordId; + + /** + * tradeNo + */ + private String tradeNo; + + /** + * 渠道类型 + */ + private PayChannel payChannel; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 下次处理时间 + */ + private LocalDateTime nextHandTime; + + /** + * 处理次数 + */ + private Integer handCount; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingWithdrawReview.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingWithdrawReview.java new file mode 100644 index 0000000..ae7363d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/ProcessingWithdrawReview.java @@ -0,0 +1,41 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.PayChannel; +import lombok.*; + +import java.time.LocalDateTime; + +@ToString +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_processing_withdraw_review") +public class ProcessingWithdrawReview { + + @TableId(type = IdType.AUTO) + private Long id; + + + private Long tradeId; + + /** + * tradeNo + */ + private String tradeNo; + + /** + * 渠道类型 + */ + private PayChannel payChannel; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/UserSubscribeLog.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/UserSubscribeLog.java new file mode 100644 index 0000000..bf7f82e --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/UserSubscribeLog.java @@ -0,0 +1,64 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.MemberType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "user_subscribe_log", autoResultMap = true) +@Data +public class UserSubscribeLog { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long userId; + + /** + * 原来的用户ID【场景:先登录A用户订阅,然后切换登录到B用户来进行订阅】 + */ + private Long orgUserId; + + private String platform; + + private String subscriptionId; + + /** + * 订阅套餐类型 + */ + private MemberType memberType; + + private AppStoreProduct.PERIOD period; + + /** + * 内购商品项id + */ + private String productId; + + /** + * 得到的Buff + */ + private Long buff; + + /** + * 支付金额 + */ + private Long payAmount; + + /** + * 平台抽成 + */ + private Long appStoreAmount; + + + private LocalDateTime createTime; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/UserSubscription.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/UserSubscription.java new file mode 100644 index 0000000..9c2caca --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/UserSubscription.java @@ -0,0 +1,110 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.enums.MemberType; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 用户订阅表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "user_subscription", autoResultMap = true) +public class UserSubscription { + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 所属平台 + */ + private Platform platform; + /** + * 用户Id + */ + private Long userId; + /** + * 订阅ID 订阅的唯一标识 + */ + private String subscriptionId; + /** + * 产品ID 或者说 订阅计划ID + */ + private String productId; + + /** + * 会员类型 + */ + private MemberType memberType; + + /** + * 价格周期 + */ + private AppStoreProduct.PERIOD priceType; + + /** + * 用于google 续期的token + */ + private String purchaseToken; + + + private Long refundTimeMs; + + /** + * 购买时间 + */ + private LocalDateTime purchaseTime; + + /** + * 过期时间 + */ + private LocalDateTime expTime; + + /** + * 自动续期状态 + */ + private Boolean autoRenewStatus; + /** + * 订阅状态 + */ + private String status; + + /** + * IP 地址 用于发生争议之后的退款证据 + */ + private String ip; + + /** + * 是否已经提醒过。 + */ + private Boolean remind; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + /** + * 编辑时间 + */ + private LocalDateTime editTime; + + + @Getter + public enum Platform { + STRIPE("web"), + APPLE("iOS"), + GOOGLE("android"), + ; + private String desc; + + Platform(String desc) { + this.desc = desc; + } + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/UserSubscriptionNotify.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/UserSubscriptionNotify.java new file mode 100644 index 0000000..558bda5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/UserSubscriptionNotify.java @@ -0,0 +1,45 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 订阅回调记录表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "user_subscription_notify", autoResultMap = true) +public class UserSubscriptionNotify { + @TableId(type = IdType.AUTO) + private Long id; + private Platform platform; + private String messageId; + /** + * 苹果退款表的主键ID + */ + private Long appleRefundRecordId; + private String content; + private String type; + private STATUS status; + private String subscriptionId; + private LocalDateTime createTime; + private LocalDateTime editTime; + private String extend; + + public enum Platform { + STRIPE, IOS, GOOGLE + } + + public enum STATUS { + PROCESSING, FINISHED, FAIL + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/WithdrawRequest.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/WithdrawRequest.java new file mode 100644 index 0000000..20d7241 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/entity/WithdrawRequest.java @@ -0,0 +1,85 @@ +package com.sonic.lion.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.sonic.lion.domain.enums.CoinType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_withdraw_request") +public class WithdrawRequest { + @TableId(type = IdType.AUTO) + private Long id; + private String platform; + private LocalDateTime createTime; //申请时间 + private LocalDateTime updateTime; //申请时间 + private int status; //申请处理状态 0 申请中 1已经处理 + /** + * 外部交易号(如订单号) + */ + private String outTradeNo; + + /** + * 交易标题或名称 + */ + private String name; + + /** + * 本方(发起方)账户(account .id) + */ + private Long srcAccountId; + + /** + * 本方(发起方)账户名称 + */ + private String srcAccountName; + + /** + * 收款方账号编号 + */ + private String desAccountNo; + + /** + * 对方(目标)账号-名称或姓名 + */ + private String desAccountName; + + /** + * 收款方账号类型:参看DesAccountType + */ + private Integer desAccountType; + + /** + * 渠道ID + */ + private Integer payChannel; + + /** + * 交易金额,单位:分 + */ + private Long amount; + + /** + * 备注 + */ + private String remark; + + /** + * 业务类型 + */ + private CoinType coinType; + + /** + * 货币类型 + */ + private String currencyType; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/AccountFundStatus.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/AccountFundStatus.java new file mode 100644 index 0000000..acea2c8 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/AccountFundStatus.java @@ -0,0 +1,38 @@ +package com.sonic.lion.domain.enums; + +/** + * @author: code + * @date: 2025/06/01 + * @Description: 用户资金账户状态 + * @version: 1.0.0 + */ +public enum AccountFundStatus { + + /** + * 正常 + */ + NORMAL(1, "NORMAL"), + + /** + * 冻结 + */ + FREEZE(2, "FREEZE"), + ; + + private final int value; + + private final String desc; + + AccountFundStatus(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BillStatus.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BillStatus.java new file mode 100644 index 0000000..2aa62c7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BillStatus.java @@ -0,0 +1,50 @@ +package com.sonic.lion.domain.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; + +/** + * @author: code + * @date: 2025/05/07 + * @Description: 交易类型 + * @version: 1.0.0 + */ +public enum BillStatus { + + INIT(0, "提交中"), + + PROCESSING(1, "处理中"), + + SUCC(2, "成功"), + + FAIL(3, "失败"), + + CANCEL(4, "取消"), + + UNKNOW(5, "未知(或异常)状态"); + + private final int value; + + private final String desc; + + BillStatus(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static BillStatus get(int value) { + for (BillStatus billStatus : values()) { + if (billStatus.getValue() == value) { + return billStatus; + } + } + return null; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BillStatusEnum.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BillStatusEnum.java new file mode 100644 index 0000000..7624e27 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BillStatusEnum.java @@ -0,0 +1,33 @@ +package com.sonic.lion.domain.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import lombok.Getter; + +/** + * 资金账单 状态 + */ +@Getter +public enum BillStatusEnum { + /** + * 初始化 刚创建 + */ + CREATED(0, "CREATED"), + /** + * 回滚 (系统错误,或者对账,或者其他原因导致 账单作废) + */ + ROLL_BACK(1, "ROLL_BACK"), + /** + * 已结算(结算之后的资金 才能被入账 ,否则在冻结状态) + */ + SETTLED(2, "SETTLED"), + ; + + private final int value; + + private final String desc; + + BillStatusEnum(int value, String desc) { + this.value = value; + this.desc = desc; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BizLogTypeEnum.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BizLogTypeEnum.java new file mode 100644 index 0000000..cfd405d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BizLogTypeEnum.java @@ -0,0 +1,16 @@ +package com.sonic.lion.domain.enums; + +/** + * 支付类 业务异常枚举 + */ +public enum BizLogTypeEnum { + IAP_UPLOADRECEIPT, + GOOGLE_UPLOADRECEIPT, + GOOGLE_CHECK, + WEBHOOK, + CHECK_OUT, + PAYOUT, + AIRWALLEX_PAYOUT, + CHECK_PAYOUT, + REFUND +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BuffAmountTypeEnum.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BuffAmountTypeEnum.java new file mode 100644 index 0000000..0e2b3a3 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BuffAmountTypeEnum.java @@ -0,0 +1,11 @@ +package com.sonic.lion.domain.enums; + +import lombok.Getter; + +@Getter +public enum BuffAmountTypeEnum { + //余额 + BALANCE, + //收入 + INCOME, +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BuffClassifyEnum.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BuffClassifyEnum.java new file mode 100644 index 0000000..da6676a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BuffClassifyEnum.java @@ -0,0 +1,198 @@ +package com.sonic.lion.domain.enums; + +import com.google.common.collect.Lists; +import com.sonic.common.utils.LocaleUtils; +import com.sonic.common.utils.MessageUtils; +import com.sonic.lion.enums.BizType; +import lombok.Getter; + +import java.util.List; + + +@Getter +public enum BuffClassifyEnum { + + //充值 biz_type = 600 AND in_or_out = 1 + B_CHARGE(BuffAmountTypeEnum.BALANCE, InOrOut.IN, "BUFF_BALANCE_TYPE_CHARGE", Lists.newArrayList( + BizType.CHARGE + )), + + //游戏购买,订单赠送 biz_type IN (100, 201) AND in_or_out = 2 + B_PURCHASE(BuffAmountTypeEnum.BALANCE, InOrOut.OUT, "BUFF_BALANCE_TYPE_PURCHASE", Lists.newArrayList( + + )), + + //游戏退款,商家订阅退款,赠送订单退款 biz_type IN (217,400,401, 402) AND in_or_out = 1 + B_REFUND(BuffAmountTypeEnum.BALANCE, InOrOut.IN, "BUFF_BALANCE_TYPE_REFUND", Lists.newArrayList( + BizType.REFUND + )), + + //biz_type IN (101, 203, 801, 802, 803, 902) AND in_or_out = 2 + B_TIP(BuffAmountTypeEnum.BALANCE, InOrOut.OUT, "BUFF_BALANCE_TYPE_TIP", Lists.newArrayList( + + )), + + //biz_type IN (219, 800, 901, 225) AND in_or_out = 2 + B_GIFT(BuffAmountTypeEnum.BALANCE, InOrOut.OUT, "BUFF_BALANCE_TYPE_GIFT", Lists.newArrayList( + + )), + + //商户订阅 biz_type = 216 AND in_or_out = 2 + B_SONIC_SUBSCRIPTION(BuffAmountTypeEnum.BALANCE, InOrOut.OUT, "BUFF_BALANCE_TYPE_SONIC_SUBSCRIPTION", Lists.newArrayList( + + )), + + //抽獎 biz_type = 704 AND in_or_out = 2 + B_LUCKY_DRAW(BuffAmountTypeEnum.BALANCE, InOrOut.OUT, "BUFF_BALANCE_TYPE_LUCKY_DRAW", Lists.newArrayList( + + )), + + //商城商品购买 biz_type = 224 AND in_or_out = 2 + B_STORE(BuffAmountTypeEnum.BALANCE, InOrOut.OUT, "BUFF_BALANCE_TYPE_STORE", Lists.newArrayList( + + )), + + //biz_type IN (200, 206, 207, 209, 212, 213, 215) AND in_or_out = 1 + B_SONIC_PRIZE(BuffAmountTypeEnum.BALANCE, InOrOut.IN, "BUFF_BALANCE_TYPE_SONIC_PRIZE", Lists.newArrayList( + + )), + + //biz_type IN (204, 205, 210, 211, 700, 701, 702, 703) + B_OTHER(BuffAmountTypeEnum.BALANCE, null, "BUFF_BALANCE_TYPE_OTHER", Lists.newArrayList( + BizType.FROZEN_BALANCE, + BizType.UNFROZEN_BALANCE + )), + + + //biz_type = 500 + I_WITHDRAW(BuffAmountTypeEnum.INCOME, null, "BUFF_INCOME_TYPE_WITHDRAW", Lists.newArrayList( + BizType.WITHDRAW + )), + + //biz_type in (100,201) AND in_or_out = 1 + I_SERVICE_INCOME(BuffAmountTypeEnum.INCOME, InOrOut.IN, "BUFF_INCOME_TYPE_SERVICE_INCOME", Lists.newArrayList( + + + )), + + //biz_type IN (400, 401, 402) AND in_or_out = 2 + I_REFUND(BuffAmountTypeEnum.INCOME, InOrOut.OUT, "BUFF_INCOME_TYPE_REFUND", Lists.newArrayList( + BizType.REFUND + )), + + //biz_type IN (101, 203, 801, 802, 803, 902) AND in_or_out = 1 + I_TIP_INCOME(BuffAmountTypeEnum.INCOME, InOrOut.IN, "BUFF_INCOME_TYPE_TIP_INCOME", Lists.newArrayList( + + )), + + //biz_type IN (219, 800, 901) AND in_or_out = 1 + I_GIFT_INCOME(BuffAmountTypeEnum.INCOME, InOrOut.IN, "BUFF_INCOME_TYPE_GIFT_INCOME", Lists.newArrayList( + + )), + + //biz_type = 216 AND in_or_out = 1 + I_SUBSCRIPTION_INCOME(BuffAmountTypeEnum.INCOME, InOrOut.IN, "BUFF_INCOME_TYPE_SUBSCRIPTION_INCOME", Lists.newArrayList( + + )), + + //biz_type IN (202, 218) AND in_or_out = 1 + I_SONIC_REWARD(BuffAmountTypeEnum.INCOME, InOrOut.IN, "BUFF_INCOME_TYPE_SONIC_REWARD", Lists.newArrayList( + + )), + + //biz_type IN (208, 214) AND in_or_out = 1 + I_STORE_REWARD(BuffAmountTypeEnum.INCOME, InOrOut.IN, "BUFF_INCOME_TYPE_STORE_REWARD", Lists.newArrayList( + + )), + //biz_type IN (601, 220) AND in_or_out = 1 + I_OTHER(BuffAmountTypeEnum.INCOME, InOrOut.IN, "BUFF_INCOME_OTHER_INCOME", Lists.newArrayList( + + )), + + ; + + /** + * 所属大类 是Banlanc 或Income + */ + private BuffAmountTypeEnum type; + + private String desc; + + /** + * 所属业务类型 + */ + private List bizTypeList; + + /** + * 收入或支出 + */ + private InOrOut inOrOut; + + BuffClassifyEnum(BuffAmountTypeEnum type, InOrOut inOrOut, String desc, List bizTypeList) { + this.type = type; + this.desc = desc; + this.bizTypeList = bizTypeList; + this.inOrOut = inOrOut; + } + + public String getDesc() { + return MessageUtils.getMessage(desc, LocaleUtils.getLocale()); + } + + /** + * 获取对应金额类型下的归类 + * + * @param type + * @return + */ + public static List getBuffClassifyList(BuffAmountTypeEnum type) { + List arrayList = Lists.newArrayList(); + for (BuffClassifyEnum value : BuffClassifyEnum.values()) { + if (value.type.equals(type)) { + arrayList.add(value); + } + } + return arrayList; + } + + /** + * 获取流水所属归类 + * + * @param bizType + * @param inOrOut + * @return + */ + public static BuffClassifyEnum getBuffClassify(BizType bizType, InOrOut inOrOut) { + for (BuffClassifyEnum value : BuffClassifyEnum.values()) { + List bizTypeList = value.getBizTypeList(); + if (bizTypeList.contains(bizType) && value.getInOrOut() != null && value.getInOrOut().equals(inOrOut)) { + return value; + } + if (value.equals(B_OTHER) && bizTypeList.contains(bizType)) { + return value; + } + if (bizType.equals(BizType.WITHDRAW) && bizTypeList.contains(BizType.WITHDRAW)) { + return value; + } + } + return null; + } + + /** + * 所有的收入类型列表(不包括打赏的类型) + */ + private static final List allIncomeBuffClassifyList = Lists.newArrayList(I_SERVICE_INCOME, I_REFUND, I_TIP_INCOME, I_SUBSCRIPTION_INCOME, I_SONIC_REWARD, I_STORE_REWARD, I_OTHER); + + /** + * 获取所有的营收业务类型(包括退款) + * @return + */ + public static List getAllIncomeBizType() { + List bizTypeList = Lists.newArrayList(); + for (BuffClassifyEnum buffClassifyEnum : allIncomeBuffClassifyList) { + bizTypeList.addAll(buffClassifyEnum.getBizTypeList()); + } + return bizTypeList; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BuffType.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BuffType.java new file mode 100644 index 0000000..73037b6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/BuffType.java @@ -0,0 +1,53 @@ +package com.sonic.lion.domain.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import lombok.Getter; + +/** + * @author: code + * @date: 2025/07/21 + * @Description: + * @version: 1.0.0 + */ +@Getter +public enum BuffType { + + /** + * 可消费Buff + */ + BALANCE(1, "BALANCE"), + + /** + * 可提现收入 + */ + WITHDRAWABLE_INCOME(2, "WITHDRAWABLE_INCOME"), + + /** + * 待入账收入 + */ + AWAITING_INCOME(3, "AWAITING_INCOME"), + + /** + * 退款buff + */ + REFUND(6, "REFUND"), + + /** + * 冻结收入 + */ + FROZEN_INCOME(4, "FROZEN_INCOME"), + + FROZEN_BALANCE(5, "FROZEN_BALANCE"), + + ; + + private final int value; + + private final String desc; + + BuffType(int value, String desc) { + this.value = value; + this.desc = desc; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/CallChannelStatus.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/CallChannelStatus.java new file mode 100644 index 0000000..5fbced5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/CallChannelStatus.java @@ -0,0 +1,312 @@ +package com.sonic.lion.domain.enums; + +import com.sonic.lion.enums.TradeStatus; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author: code + * @date: 2025/06/10 + * @Description: 调用支付渠道状态 + * @version: 1.0.0 + */ +public enum CallChannelStatus { + + /** + * 初始化,准备提交请求 + */ + INIT(0, "INIT"), + + /** + * 已提交请求,渠道处理中 + */ + PROCESSING(1, "PROCESSING"), + + /** + * 渠道处理成功 + */ + SUCC(2, "SUCC"), + + /** + * 渠道处理失败 + */ + FAIL(3, "FAIL"), + + /** + * 请求取消 + */ + CANCEL(4, "CANCEL"), + + /** + * 出现异常,情况未知 + */ + UNKNOW(5, "UNKNOW"), + + EXPIRED(6, "EXPIRED"); + + private final int value; + + private final String desc; + + CallChannelStatus(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public static CallChannelStatus fromPayssionRefund(String state) { + Map map = new HashMap() { + { + put("refunded", CallChannelStatus.SUCC); + put("refund_pending", CallChannelStatus.PROCESSING); + put("Card refund_failed", CallChannelStatus.FAIL); + put("refund_cancelled", CallChannelStatus.CANCEL); + } + }; + return map.get(state) == null ? CallChannelStatus.PROCESSING : map.get(state); + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static CallChannelStatus get(int value) { + for (CallChannelStatus callChannelStatus : values()) { + if (callChannelStatus.getValue() == value) { + return callChannelStatus; + } + } + return null; + } + + private static final Map channelTradeStatusMaps = new HashMap() { + { + put(CallChannelStatus.INIT, TradeStatus.WAITPAY); + put(CallChannelStatus.PROCESSING, TradeStatus.PROCESSING); + put(CallChannelStatus.SUCC, TradeStatus.FINISHED); + put(CallChannelStatus.FAIL, TradeStatus.CLOSED); + put(CallChannelStatus.CANCEL, TradeStatus.CLOSED); + put(CallChannelStatus.UNKNOW, TradeStatus.CLOSED); + put(CallChannelStatus.EXPIRED, TradeStatus.CLOSED); + } + }; + + + private static final Map callChannelStatusMap = new HashMap() { + { + // 创建成功 + put("CREATED", CallChannelStatus.PROCESSING); + // 订单已保存并保留 + put("SAVED", CallChannelStatus.PROCESSING); + // 支付成功 客户通过贝宝(PayPal)钱包或其他形式的客人或非品牌付款批准了付款。例如,卡,银行帐户 + put("APPROVED", CallChannelStatus.SUCC); + // 付款已被授权或已为订单捕获授权付款 + put("COMPLETED", CallChannelStatus.SUCC); + // 订单中的所有购买单位均作废 + put("VOIDED", CallChannelStatus.FAIL); + + //apture_status + // 退款已取消 + put("CANCELLED", CallChannelStatus.SUCC); + // 退款中 尚未记入收款人的PayPal帐户 + put("PENDING", CallChannelStatus.PROCESSING); + // 退款完成 记入收款人的PayPal帐户 + put("COMPLETED", CallChannelStatus.SUCC); + // 部分退款已到账 + put("PARTIALLY_REFUNDED", CallChannelStatus.SUCC); + // 已退回 + put("REFUNDED", CallChannelStatus.FAIL); + // 退款异常 + put("FAILED", CallChannelStatus.FAIL); + // 资金无法收回 + put("DECLINED", CallChannelStatus.FAIL); +// 在状态为PENDING或者DECLINED,reason的值可能是以下几种 +// +// 1.BUYER_COMPLAINT。付款人与贝宝(PayPal)对此捕获的付款提出了争议。 +// 2.CHARGEBACK。响应于付款人与用于支付此已捕获付款的金融工具的发行人对此已捕获的付款提出异议,已收回的资金被撤回。 +// 3.ECHECK。由尚未结清的电子支票支付的付款人。 +// 4.INTERNATIONAL_WITHDRAWAL。访问您的在线帐户。在您的“帐户概览”中,接受并拒绝此笔付款。 +// 5.OTHER。无法提供其他特定原因。有关此笔付款的更多信息,请在线访问您的帐户或联系PayPal。 +// 6.PENDING_REVIEW。捕获的付款正在等待人工审核。 +// 7.RECEIVING_PREFERENCE_MANDATES_MANUAL_ACTION。收款人尚未为其帐户设置适当的接收首选项。有关如何接受或拒绝此付款的更多信息,请在线访问您的帐户。通常在某些情况下提供此原因,例如当所捕获付款的货币与收款人的主要持有货币不同时。 +// 8.REFUNDED。收回的资金已退还。 +// 9.TRANSACTION_APPROVED_AWAITING_FUNDING。付款人必须将这笔付款的资金汇出。通常,此代码适用于手动EFT。 +// 10.UNILATERAL。收款人没有PayPal帐户。 +// 11.VERIFICATION_REQUIRED。收款人的PayPal帐户未通过验证。 +// + + //apture_status + + put("ONHOLD", CallChannelStatus.PROCESSING); + //payout + // 支出已取消 + put("NEW", CallChannelStatus.PROCESSING); + // 支出中 您的付款要求已收到,将尽快处理 + put("PENDING", CallChannelStatus.PROCESSING); + // 支出完成 资金已记入收件人的帐户 + put("SUCCESS", CallChannelStatus.SUCC); + // 此支出的接收者没有PayPal帐户。贝宝向收件人发送了注册链接以创建帐户。如果收款人未在30天内创建帐户并要求付款,则资金将退还到您的帐户中 + put("UNCLAIMED", CallChannelStatus.PROCESSING); + // 支出已退回 + put("REFUNDED", CallChannelStatus.FAIL); + //拒绝 + put("DENIED", CallChannelStatus.FAIL); + put("FAILURE", CallChannelStatus.FAIL); + put("BLOCKED", CallChannelStatus.FAIL); + put("REVERSED", CallChannelStatus.FAIL); + put("RETURNED", CallChannelStatus.FAIL); + } + }; + + public static TradeStatus toTradeStatus(CallChannelStatus callChannelStatus) { + return channelTradeStatusMaps.get(callChannelStatus); + } + + + public static CallChannelStatus from(String orderStatus) { + CallChannelStatus callChannelStatus = callChannelStatusMap.get(orderStatus); + return callChannelStatus == null ? CallChannelStatus.PROCESSING : callChannelStatus; + } + + public static CallChannelStatus fromCheckout(String orderStatus) { + /** + * "Pending" "Authorized" "Card Verified" "Voided" "Partially Captured" "Captured" "Partially Refunded" "Refunded" "Declined" "Canceled" "Expired" "Paid" + */ + Map map = new HashMap() { + { + put("Pending", CallChannelStatus.PROCESSING); + put("Authorized", CallChannelStatus.PROCESSING); + put("Card Verified", CallChannelStatus.PROCESSING); + + put("Voided", CallChannelStatus.FAIL); + put("Partially Captured", CallChannelStatus.PROCESSING); + put("Captured", CallChannelStatus.SUCC); + + put("Partially Refunded", CallChannelStatus.FAIL); + put("Refunded", CallChannelStatus.FAIL); + put("Declined", CallChannelStatus.FAIL); + put("Canceled", CallChannelStatus.CANCEL); + + put("Expired", CallChannelStatus.EXPIRED); + put("Paid", CallChannelStatus.PROCESSING); + } + }; + return map.get(orderStatus) == null ? CallChannelStatus.PROCESSING : map.get(orderStatus); + } + + public static CallChannelStatus fromPayssion(String orderStatus) { + /** + * error 支付发生错误 + * pending 未完成支付 + * completed 支付成功 + * paid_partial 部分支付,用户只支付了部分金额 + * awaiting_confirm 待确认,系统风控自动拦截 + * failed 支付失败 + * cancelled 交易被取消 + * expired 交易过期 + * refunded 退款成功 + * refund_pending 已申请退款,正在处理退款 + * refund_failed 退款失败 + * refund_cancelled 退款已取消 + * chargeback 拒付 + */ + Map map = new HashMap() { + { + put("error", CallChannelStatus.FAIL); + put("pending", CallChannelStatus.PROCESSING); + put("completed", CallChannelStatus.SUCC); + + put("paid_partial", CallChannelStatus.PROCESSING); + put("awaiting_confirm", CallChannelStatus.PROCESSING); + put("failed", CallChannelStatus.FAIL); + + put("cancelled", CallChannelStatus.CANCEL); + put("expired", CallChannelStatus.EXPIRED); + put("chargeback", CallChannelStatus.FAIL); + } + }; + return map.get(orderStatus) == null ? CallChannelStatus.PROCESSING : map.get(orderStatus); + } + + /** + * 将airwallex的状态转换成系统内的状态 + * @param orderStatus 一级状态 + * @param latestPaymentAttemptStatus 最新的二级子状态 + * @param latestPaymentAttemptFailureCode 最新的二级子状态对应的异常码 + * @return + */ + public static CallChannelStatus fromAirwallex(String orderStatus, String latestPaymentAttemptStatus, String latestPaymentAttemptFailureCode) { + /** + * REQUIRES_PAYMENT_METHOD + * Populate payment_method when calling confirm + * This value is returned if payment_method is either null, or the payment_method has failed during confirm, and a different payment_method should be provided. + * + * REQUIRES_CUSTOMER_ACTION + * Pending customer action, see next_action for details. Possible causes are pending 3DS authentication, QR code scan. + * + * REQUIRES_CAPTURE + * Pending manual capture, which is required after calling confirm with auto_capture=false. + * + * PENDING + * he payment request has been accepted. Waiting for the final result. + * + * CANCELLED + * The PaymentIntent has been cancelled. The amount authorized but not captured will be returned. + * + * SUCCEEDED + * The payment was successful, no further action required. + */ + Map map = new HashMap() { + { + put("REQUIRES_PAYMENT_METHOD", CallChannelStatus.PROCESSING); + + put("REQUIRES_CUSTOMER_ACTION", CallChannelStatus.PROCESSING); + put("REQUIRES_CAPTURE", CallChannelStatus.PROCESSING); + put("PENDING", CallChannelStatus.PROCESSING); + + put("CANCELLED", CallChannelStatus.CANCEL); + put("SUCCEEDED", CallChannelStatus.SUCC); + } + }; + //付款尝试状态类型列表 + //判断付款参数状态(子状态)的异常类型(当子状态为关闭、过期、失败时,直接转换对应的支付状态返回) + Map latestPaymentAttemptStatusMap = new HashMap() { + { + put("CANCELLED", CallChannelStatus.CANCEL); + put("EXPIRED", CallChannelStatus.EXPIRED); + put("FAILED", CallChannelStatus.FAIL); + } + }; + //fix_6.29.0 判断子状态如果为识别,且异常码为 时,处理成 PROCESSING 状态 【场景:用户付款先3DS验证失败,然后再验证成功并成功付款的场景】 + if("FAILED".equals(latestPaymentAttemptStatus) && "authentication_failed".equals(latestPaymentAttemptFailureCode)) { + return CallChannelStatus.PROCESSING; + } + //子状态的终态转换 + if(StringUtils.isNotEmpty(latestPaymentAttemptStatus) && latestPaymentAttemptStatusMap.get(latestPaymentAttemptStatus) != null) { + return latestPaymentAttemptStatusMap.get(latestPaymentAttemptStatus); + } + //主状态的状态转换 + CallChannelStatus callChannelStatus = map.get(orderStatus) == null ? CallChannelStatus.PROCESSING : map.get(orderStatus); + return callChannelStatus; + } + + public static CallChannelStatus fromTipalti(String status) { + Map map = new HashMap() { + { + put("Rejected", CallChannelStatus.FAIL); + put("Paid", CallChannelStatus.SUCC); + put("Cleared", CallChannelStatus.SUCC); + + put("Deferred", CallChannelStatus.FAIL); + put("Canceled", CallChannelStatus.FAIL); + put("Deferred (Internal)", CallChannelStatus.FAIL); + } + }; + return map.get(status) == null ? CallChannelStatus.PROCESSING : map.get(status); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/CoinType.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/CoinType.java new file mode 100644 index 0000000..daa0c6d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/CoinType.java @@ -0,0 +1,9 @@ +package com.sonic.lion.domain.enums; + +/** + * @author mzc + */ +public enum CoinType { + BUFF, + E_COIN; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/FreezeStatus.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/FreezeStatus.java new file mode 100644 index 0000000..af00fd6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/FreezeStatus.java @@ -0,0 +1,36 @@ +package com.sonic.lion.domain.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; + +public enum FreezeStatus { + + FREEZE(1, "冻结"), + + UNFREEZE(2, "已解冻"); + + private final int value; + + private final String desc; + + FreezeStatus(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static FreezeStatus get(int value) { + for (FreezeStatus freezeStatus : values()) { + if (freezeStatus.getValue() == value) { + return freezeStatus; + } + } + return null; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/FreezeType.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/FreezeType.java new file mode 100644 index 0000000..5787d13 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/FreezeType.java @@ -0,0 +1,54 @@ +package com.sonic.lion.domain.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; + +/** + * @author: code + * @date: 2025/05/11 + * @Description: + * @version: 1.0.0 + */ +public enum FreezeType { + + WITHDRAW(1, "提现"), + + REFUND(2, "退款"), + + TRADE(3, "交易"), + + ACCOUNT(4, "帐户冻结"), + + TRANSFER(5, "转账"), + + RISK_CONTROL(10, "风控"), + + ABNORMAL(11, "异常"), + + OTHERS(12, "其他"); + + private final int value; + + private final String desc; + + FreezeType(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static FreezeType get(int value) { + for (FreezeType freezeType : values()) { + if (freezeType.getValue() == value) { + return freezeType; + } + } + return null; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/InOrOut.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/InOrOut.java new file mode 100644 index 0000000..78a719f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/InOrOut.java @@ -0,0 +1,51 @@ +package com.sonic.lion.domain.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; + + +/** + * @author: code + * @date: 2025/05/14 + * @Description: + * @version: 1.0.0 + */ +public enum InOrOut { + + IN(1, "收入"), + + OUT(2, "支出"), + ; + private final int value; + + private final String desc; + + InOrOut(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static InOrOut get(int value) { + for (InOrOut inOrOut : values()) { + if (inOrOut.getValue() == value) { + return inOrOut; + } + } + return null; + } + + public String getDescInEnglish() { + if (value == 1) { + return "Income"; + } + return "Outcome"; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/ProductRewardType.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/ProductRewardType.java new file mode 100644 index 0000000..c967a09 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/ProductRewardType.java @@ -0,0 +1,42 @@ +package com.sonic.lion.domain.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 增加一个类型 枚举: 首充 , 周五周五限时优惠 + * 增加一个优惠折扣比例  0.1 + * + * @author code + */ +@AllArgsConstructor +@Getter +public enum ProductRewardType { + /** + * 首充 + */ + FIRST_CHARGE(0.2 , 20), + /** + * 每周五限时优惠 + */ + EVERY_FRIDAY(0.1,10), + + /** + * 每月累计充值达到要求 + */ + MONTH_REWARD(0D,0), + + /** + * 热门 + */ + HOT(0D,0), + + /** + * 6.23.0 高档位充送 + */ + LARGE_PRODUCT(0D, 0), + + ; + private Double preferentialRatio; + private long ratio100; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/ProductType.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/ProductType.java new file mode 100644 index 0000000..21bed07 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/ProductType.java @@ -0,0 +1,5 @@ +package com.sonic.lion.domain.enums; + +public enum ProductType { + APP , SUBSCRIPTION +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/SystemUser.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/SystemUser.java new file mode 100644 index 0000000..821bab1 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/SystemUser.java @@ -0,0 +1,62 @@ +package com.sonic.lion.domain.enums; + +/** + * @author: code + * @date: 2025/06/19 + * @Description: 分配给用户做对手账户的系统账户, 系统账号个数需要为2的n次幂 + * @version: 1.0.0 + */ +public enum SystemUser { + + SYSTEM_USER_1(-1, "SYSTEM_USER_1"), + + SYSTEM_USER_2(-2, "SYSTEM_USER_2"), + + SYSTEM_USER_3(-3, "SYSTEM_USER_3"), + + SYSTEM_USER_4(-4, "SYSTEM_USER_4"), + + SYSTEM_USER_5(-5, "SYSTEM_USER_5"), + + SYSTEM_USER_6(-6, "SYSTEM_USER_6"), + + SYSTEM_USER_7(-7, "SYSTEM_USER_7"), + + SYSTEM_USER_8(-8, "SYSTEM_USER_8"), + ; + + /** + * 系统用户uid + */ + private final long value; + + /** + * 系统用户名称 + */ + private final String desc; + + private static final int N = values().length - 1; + + SystemUser(long value, String desc) { + this.value = value; + this.desc = desc; + } + + public long getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + /** + * 根据用户id获取一个系统账号做交易对手账号 + * + * @param uid + * @return + */ + public static SystemUser getByUid(long uid) { + return values()[(int) (uid & N)]; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/TradeEvent.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/TradeEvent.java new file mode 100644 index 0000000..e2a277a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/TradeEvent.java @@ -0,0 +1,71 @@ +package com.sonic.lion.domain.enums; + +import com.sonic.lion.enums.BizType; + +/** + * @author: code + * @date: 2025/07/09 + * @Description: 交易事件 + * @version: 1.0.0 + */ +public enum TradeEvent { + + /** + * 付款 + */ + PAYMENT(1, "PAYMENT"), + + /** + * 提现 + */ + WITHDRAW(2, "WITHDRAW"), + + /** + * 退款 + */ + REFUND(3, "REFUND"), + + /** + * 绑定账户 + */ + BIND_ACCOUNT(4, "BIND_ACCOUNT"), + ; + + private final int value; + + private final String desc; + + TradeEvent(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static TradeEvent get(int value) { + for (TradeEvent tradeType : values()) { + if (tradeType.getValue() == value) { + return tradeType; + } + } + return null; + } + + public static TradeEvent get(BizType bizType) { + if (bizType == BizType.CHARGE) { + return PAYMENT; + } else if (bizType == BizType.REFUND) { + return REFUND; + } else if (bizType == BizType.WITHDRAW) { + return WITHDRAW; + } else { + return null; + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/UserThirdStatus.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/UserThirdStatus.java new file mode 100644 index 0000000..bb9b205 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/enums/UserThirdStatus.java @@ -0,0 +1,38 @@ +package com.sonic.lion.domain.enums; + +/** + * @author: code + * @date: 2025/06/01 + * @Description: 用户三方账号状态 + * @version: 1.0.0 + */ +public enum UserThirdStatus { + + /** + * 打开 + */ + ENABLED(1, "ENABLED"), + + /** + * 关闭 + */ + DISABLED(2, "DISABLED"), + ; + + private final int value; + + private final String desc; + + UserThirdStatus(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AccountGenerateInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AccountGenerateInput.java new file mode 100644 index 0000000..1b5c383 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AccountGenerateInput.java @@ -0,0 +1,44 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AccountGenerateInput { + + private Long uid; + + private Long accountFundId; + + private PayChannel payChannel; + + private String firstName; + + private String middleName; + + private String lastName; + + private String email; + + private String addressLine1; + + private String city; + + private String stateProvince; + + private String country; + + private String postalCode; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AdminGiftBuffListInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AdminGiftBuffListInput.java new file mode 100644 index 0000000..73501f0 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AdminGiftBuffListInput.java @@ -0,0 +1,14 @@ +package com.sonic.lion.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class AdminGiftBuffListInput { + + @ApiModelProperty("页码") + private Integer pn; + + @ApiModelProperty("每页数量") + private Integer ps; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AirwallexCreateOrderReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AirwallexCreateOrderReq.java new file mode 100644 index 0000000..df5521b --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AirwallexCreateOrderReq.java @@ -0,0 +1,52 @@ +package com.sonic.lion.domain.input; + +import lombok.Data; +import lombok.ToString; + +import java.util.Map; + +@ToString +@Data +public class AirwallexCreateOrderReq { + + /** + * 货币金额 + */ + private String amount; + + /** + * 货币类型 + */ + private String currency = "USD"; + + /** + * 描述信息 + */ + private String descriptor; + + /** + * 商家订单ID(tradeNo) + */ + private String merchant_order_id; + + /** + * 请求的唯一ID + */ + private String request_id; + + /** + * 支付返回后跳转的页面URL + */ + private String return_url; + + /** + * 用户ID + */ + private String customer_id; + + /** + * 商品信息 + */ + Map order; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AirwallexCreatePaymentsInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AirwallexCreatePaymentsInput.java new file mode 100644 index 0000000..970ef41 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AirwallexCreatePaymentsInput.java @@ -0,0 +1,103 @@ +package com.sonic.lion.domain.input; + +import lombok.Data; + +/** + * @Author code + * @Description TODO + * @Date 2024/9/19 14:38 + * @Version 1.0 + */ +@Data +public class AirwallexCreatePaymentsInput { + + /** + * 受益人ID + */ + private String beneficiary_id; + + /** + * 您可以使用的自由文本字段;可以填充付款可能需要的任何其他标识符 + */ + private String client_data; + + /** + * 表示是否 PAYER / BENEFICIARY 将要承担支付费用,并影响受益人实际收到的金额 + */ + private String fee_paid_by = "BENEFICIARY"; + + /** + * 一组键值对,用于将您自己的数据与付款一起存储 + */ + private Object metadata; + + /** + * 付款人 + */ + @Deprecated + private Object payer; + + /** + * 付款人ID 和付款人二选一 + */ + private String payer_id; + + /** + * 付款金额 + */ + private String payment_amount; + + /** + * 付款货币,即收款人收到的货币(3 个字母的 ISO-4217 代码) + */ + private String payment_currency; + + /** + * 付款日期 + */ + private String payment_date; + + /** + * 付款方式,即 SWIFT、LOCAL + */ + private String payment_method = "LOCAL"; + + /** + * 提供 valid quote_id 以在报价 client_rate 中提供的位置执行转换 + */ + private String quote_id; + + /** + * 付款指示的原因 + * 应为以下值之一:audio_visual_services, bill_payment, business_expenses, construction, donation_charitable_contribution, education_training, family_support, freight, + * goods_purchased, investment_capital, investment_proceeds, living_expenses, loan_credit_repayment, medical_services, pension, personal_remittance, professional_business_services, + * real_estate, taxes, technical_services, transfer_to_own_account, travel, wages_salary, other_services + */ + private String reason; + + /** + * 用户指定的参考,将在与收款人银行的付款交易中向收款人显示 + */ + private String reference; + + /** + * UUID + */ + private String request_id; + + /** + * 源金额 + */ + private String source_amount; + + /** + * 源货币,即付款人用于付款的货币(3 个字母的 ISO-4217 代码) + */ + private String source_currency; + + /** + * (仅适用于 SWIFT 付款),指定谁应承担 SWIFT 费用,( SHARED 默认)或 PAYER + */ + private String swift_charge_option; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AvailableChannelInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AvailableChannelInput.java new file mode 100644 index 0000000..3ee864d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/AvailableChannelInput.java @@ -0,0 +1,24 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.BizType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/07/27 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AvailableChannelInput { + + private BizType bizType; + + private String platform; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BeneficiaryInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BeneficiaryInput.java new file mode 100644 index 0000000..210deb7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BeneficiaryInput.java @@ -0,0 +1,59 @@ +package com.sonic.lion.domain.input; + +import lombok.Data; + +/** + * @Author code + * @Description TODO + * @Date 2024/9/19 13:55 + * @Version 1.0 + */ +@Data +public class BeneficiaryInput { + + /** + * 【必填】 + * 有关受益人的其他信息 + */ + private Object additional_info; + + /** + * 【必填】 + * 收款人的地址详情 + */ + private Object address; + + /** + * 【必填】 + * 收款人的银行账户详细信息,付款将存入其中 + */ + private Object bank_details; + + /** + * 收款人的公司名称 + */ + private String company_name; + + + /** + * 受益人出生日期 + */ + private String date_of_birth; + + /** + * 【必填】 + * 受益人的实体类型 + */ + private String entity_type; + +// /** +// * 受益人的名字 +// */ +// private String first_name; +// +// /** +// * 受益人的姓氏 +// */ +// private String last_name; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BillListV2Input.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BillListV2Input.java new file mode 100644 index 0000000..bd8d4ac --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BillListV2Input.java @@ -0,0 +1,47 @@ +package com.sonic.lion.domain.input; + +import com.sonic.common.rpc.Page; +import com.sonic.lion.domain.enums.BuffClassifyEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2023-11-23 16:58 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class BillListV2Input { + + @ApiModelProperty("开始时间戳") + private Long startTime; + + @ApiModelProperty("结束时间戳") + private Long endTime; + + @ApiModelProperty("模糊查询出用户列表后,选中某个用户id,则传该字段") + private Long userId; + + @ApiModelProperty("模糊查询如果是订单号或交易号,则传该字段") + private String bizNo; + + @ApiModelProperty("Buff Balance类型列表") + private List buffBalanceTypeList; + + @ApiModelProperty("Buff Income类型列表") + private List buffIncomeTypeList; + + @ApiModelProperty("收入状态类型 可提现:2,待入账:3") + private Integer buffType; + + Page page = new Page<>(1, 10); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BindBankAccountInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BindBankAccountInput.java new file mode 100644 index 0000000..cd6166d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BindBankAccountInput.java @@ -0,0 +1,28 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/08 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BindBankAccountInput { + + private Long uid; + + private PayChannel payChannel; + + private String transferMethodCountry; + + private String email; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BindBankCardInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BindBankCardInput.java new file mode 100644 index 0000000..873cb50 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BindBankCardInput.java @@ -0,0 +1,32 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BindBankCardInput { + + private Long uid; + + private PayChannel payChannel; + + private String cardNumber; + + private Date dateOfExpiry; + + private String cvv; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BindPaypalAccountInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BindPaypalAccountInput.java new file mode 100644 index 0000000..a30e4f3 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BindPaypalAccountInput.java @@ -0,0 +1,28 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/08 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BindPaypalAccountInput { + + private Long uid; + + private PayChannel payChannel; + + private String transferMethodCountry; + + private String email; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffChangeInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffChangeInput.java new file mode 100644 index 0000000..b6def89 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffChangeInput.java @@ -0,0 +1,94 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * @author: code + * @date: 2025/07/17 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class BuffChangeInput { + + /** + * 平台代码 + */ + private String platform; + + /** + * 变动金额的用户id + */ + private Long uid; + + /** + * 变动的buff + */ + private Long buff; + + /** + * 赠送金额 + */ + private Long giftAmount; + + /** + * 对手方用户id + */ + private Long desUid; + + /** + * 支付渠道 + */ + private PayChannel payChannel; + + /** + * 交易号 + */ + private String tradeNo; + + /** + * 业务订单号 + */ + private String bizNo; + + /** + * 外部交易号关联单号(如:打赏单号关联的游戏订单的订单号) + */ + private String bizNoRelationNo; + + /** + * 交易类型 + */ + private BizType bizType; + + private String errorMessage; + + private String reason; + + private String extend; + + private String payMethod; + + /** + * 转入可提现收入时间 + */ + private LocalDateTime toWithdrawableIncomeTime; + + /** + * 游戏订单 担保交易 冗余目标方的用户id + */ + private Long targetUserId; + + /** + * 是否全额退款 + */ + private Boolean fullRefund; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffTransferExtend.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffTransferExtend.java new file mode 100644 index 0000000..fa747a8 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffTransferExtend.java @@ -0,0 +1,79 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.domain.entity.AppStoreProduct; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @author code + * 转账所需所有 额外字段。 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BuffTransferExtend { + + /** + * 发生的 用户Id + */ + private Long uid; + + /** + * 对手方 用户Id + */ + private Long desUid; + +// /** +// * 发生的Buff数量 +// */ +// private Long buff; + + /** + * 业务类型 + */ + private BizType bizType; + + /** + * 支付渠道 + */ + private PayChannel payChannel; + + /** + * 交易号 + */ + private String tradeNo; + + /** + * 业务编号 + */ + private String bizNo; + + /** + * 外部交易号关联单号(如:打赏单号关联的游戏订单的订单号) + */ + private String bizNoRelationNo; + + /** + * 原因 + */ + private String reason; + + /** + * 订阅时长 (待提现天数) + */ + private AppStoreProduct.PERIOD period; + + + @ApiModelProperty(value = "如果是订阅,则为订阅开始时间,如果是续订,则为上次的订阅的过期时间") + private LocalDateTime startTime; + + private String extend; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffTransferInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffTransferInput.java new file mode 100644 index 0000000..16fcbaa --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffTransferInput.java @@ -0,0 +1,80 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.domain.enums.BuffType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/07/17 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BuffTransferInput { + + private String platform; + + private Long decUid; + + private BuffType decBuffType; + + private Long decBuff; + + private Long addUid; + + private BuffType addBuffType; + + private Long addBuff; + + private Long addSystemUid; + + private BuffType addSystemBuffType; + + private Long addSystemBuff; + + private PayChannel payChannel; + + private BizType bizType; + + private String tradeNo; + + private String bizNo; + + /** + * 外部交易号关联单号(如:打赏单号关联的游戏订单的订单号) + */ + private String bizNoRelationNo; + + private String errorMessage; + + private String reason; + + private Long giftAmount; + + private String payMethod; + + private String extend; + + /** + * 游戏订单 担保交易 冗余目标方的用户id + */ + private Long addTargetUserId; + + /** + * 游戏订单 担保交易 冗余目标方的用户id + */ + private Long decTargetUserId; + + /** + * 是否全额退款 + */ + private Boolean fullRefund; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffTransferTargetInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffTransferTargetInput.java new file mode 100644 index 0000000..49fd3fe --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/BuffTransferTargetInput.java @@ -0,0 +1,42 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.domain.enums.BuffType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author code + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BuffTransferTargetInput { + +// /** +// * 账户Id +// */ +// private Long accountId; + + /** + * 用户ID + */ + private Long uid; + + /** + * 账户类型 + */ + private BuffType decBuffType; + + /** + * 具体金额 + */ + private Long amount; + +// /** +// * 待入账的时间 天数 +// */ +// private long onGoingWithdrawAbleDays; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelBindBankAccountInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelBindBankAccountInput.java new file mode 100644 index 0000000..51293bf --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelBindBankAccountInput.java @@ -0,0 +1,28 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelBindBankAccountInput { + + private PayChannel payChannel; + + private String thirdAccountOpenId; + + private String transferMethodCountry; + + private String email; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelBindBankCardInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelBindBankCardInput.java new file mode 100644 index 0000000..dc82aa9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelBindBankCardInput.java @@ -0,0 +1,32 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * @author: code + * @date: 2025/06/05 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelBindBankCardInput { + + private PayChannel payChannel; + + private String thirdAccountOpenId; + + private String cardNumber; + + private Date dateOfExpiry; + + private String cvv; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelBindPaypalInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelBindPaypalInput.java new file mode 100644 index 0000000..339e9dc --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelBindPaypalInput.java @@ -0,0 +1,28 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/05 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelBindPaypalInput { + + private PayChannel payChannel; + + private String thirdAccountOpenId; + + private String transferMethodCountry; + + private String email; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCancelInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCancelInput.java new file mode 100644 index 0000000..f11709d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCancelInput.java @@ -0,0 +1,24 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/15 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelCancelInput { + + private PayChannel payChannel; + + private String transactionId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCheckPaymentInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCheckPaymentInput.java new file mode 100644 index 0000000..b5935a1 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCheckPaymentInput.java @@ -0,0 +1,25 @@ +package com.sonic.lion.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/07/07 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelCheckPaymentInput { + + private String batchId; + + private String transactionId; + + private String tradeNo; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCheckRefundInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCheckRefundInput.java new file mode 100644 index 0000000..be90d3f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCheckRefundInput.java @@ -0,0 +1,21 @@ +package com.sonic.lion.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/07/08 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelCheckRefundInput { + + private String batchId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelConfirmTransferInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelConfirmTransferInput.java new file mode 100644 index 0000000..c69c53a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelConfirmTransferInput.java @@ -0,0 +1,21 @@ +package com.sonic.lion.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/11 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelConfirmTransferInput { + + private String transferToken; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCreateCustomerInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCreateCustomerInput.java new file mode 100644 index 0000000..f1676e2 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelCreateCustomerInput.java @@ -0,0 +1,46 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelCreateCustomerInput { + + private Long userId; + + private PayChannel payChannel; + + private Long accountFundId; + + private String firstName; + + private String middleName; + + private String lastName; + + private String name; + + private String email; + + private String addressLine1; + + private String city; + + private String stateProvince; + + private String country; + + private String postalCode; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelPaymentInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelPaymentInput.java new file mode 100644 index 0000000..2c171c8 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelPaymentInput.java @@ -0,0 +1,43 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.domain.entity.PayTrade; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/05/18 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelPaymentInput { + + private String submitId; + + /** + * braintree支付需要参数,由客户端生成 + */ + private String nonce; + + private PayTrade payTrade; + + private String returnUrl; + + private String cancelUrl; + + private PaymentBillInfo paymentBillInfo; + + private String ip; + + /** + * 支付方式 (payssion) + */ + private String pmId; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelPayoutInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelPayoutInput.java new file mode 100644 index 0000000..d84c2a6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelPayoutInput.java @@ -0,0 +1,58 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.domain.entity.PayTrade; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; + +@Data +@Builder +@ToString +public class ChannelPayoutInput { + + private PayChannel payChannel; + + /** + * 交易号 + */ + private String tradeNo; + + /** + * 对方(渠道)账号 (如:银行卡号,支付宝账号,资金账号) + */ + private String desAccountNo; + + /** + * 渠道提交编号 + */ + private String submitId; + + /** + * 金额,单位:分(用户到账的 净值 实际到账金额) + */ + private Long amount; + + /** + * 备注 + */ + private String remark; + + private PayTrade payTrade; + + private BizType bizType; + + + private Long userId; + + private Long tradeId; + + /** + * 三方费用 + * Paypal 是后置收费 + * tipalti 是 先收费 后转账 + */ + private Long thirdFee; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelRefundInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelRefundInput.java new file mode 100644 index 0000000..23340fc --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelRefundInput.java @@ -0,0 +1,29 @@ +package com.sonic.lion.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/07/06 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelRefundInput { + + /** + * 渠道交易号 + */ + private String transactionId; + + /** + * 退款金额 + */ + private Long amount; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelSubPayoutInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelSubPayoutInput.java new file mode 100644 index 0000000..1214b56 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelSubPayoutInput.java @@ -0,0 +1,21 @@ +package com.sonic.lion.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 渠道子账户订阅 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelSubPayoutInput { + + private String customerId; + + + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelTransferInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelTransferInput.java new file mode 100644 index 0000000..51a272e --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelTransferInput.java @@ -0,0 +1,52 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/11 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelTransferInput { + + private PayChannel payChannel; + + /** + * 交易号 + */ + private String tradeNo; + + /** + * 用户渠道账户 + */ + private String srcAccountNo; + + /** + * 用户账号 (如:银行卡号,支付宝账号,资金账号) + */ + private String desAccountNo; + + /** + * 渠道提交编号 + */ + private String submitId; + + /** + * 金额,单位:分 + */ + private Long amount; + + /** + * 备注 + */ + private String remark; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelWebhookInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelWebhookInput.java new file mode 100644 index 0000000..23948fb --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChannelWebhookInput.java @@ -0,0 +1,34 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelWebhookInput { + + private String methodName; + + private PayChannel payChannel; + + private Object signature; + + private String timestamp; + + private Object payload; + + private String tradeNo; + + private String secretKey; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChatRoomRewardPlatformFeeInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChatRoomRewardPlatformFeeInput.java new file mode 100644 index 0000000..adbb10c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/ChatRoomRewardPlatformFeeInput.java @@ -0,0 +1,26 @@ +package com.sonic.lion.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 礼物打赏手续费 + * + * @author mzc + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ChatRoomRewardPlatformFeeInput { + + @ApiModelProperty("被打赏的人") + private Long userId; + + @ApiModelProperty("用户所得金额") + private Long userAmount; + + @ApiModelProperty("平台手续费金额") + private Long platformFee; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/CheckoutInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/CheckoutInput.java new file mode 100644 index 0000000..1073659 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/CheckoutInput.java @@ -0,0 +1,34 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.PaymentType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/05/20 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CheckoutInput { + + private Long uid; + + private String tradeNo; + + private PayChannel payChannel; + + private PaymentType paymentType; + + private String returnUrl; + + private String cancelUrl; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/CreateBeneficiaryInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/CreateBeneficiaryInput.java new file mode 100644 index 0000000..0792c53 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/CreateBeneficiaryInput.java @@ -0,0 +1,43 @@ +package com.sonic.lion.domain.input; + +import com.google.api.client.util.Lists; +import lombok.Data; + +import java.util.List; + +/** + * @Author code + * @Description TODO + * @Date 2024/9/19 11:17 + * @Version 1.0 + */ +@Data +public class CreateBeneficiaryInput { + + /** + * 【必填】 + * 受益人的详细信息 + */ + private BeneficiaryInput beneficiary; + + /** + * 受益人的昵称 + */ + private String nickname; + + /** + * 将用于向此受益人付款的付款人实体类型。出于合规性原因,这是准确捕获和验证适当数据所必需的(COMPANY | PERSONAL)。默认为 COMPANY + */ + private String payer_entity_type = "COMPANY"; + + /** + * 付款方式,即 SWIFT、LOCAL。需要指定付款方式,以确保为指定的付款方式捕获和验证准确的银行详细信息。这是及时准确地将资金汇到收款人账户所必需的 + */ + private List payment_methods = Lists.newArrayList(); + + /** + * 向该收款人发出付款指示的原因 + */ + private String payment_reason; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/CreateSubscribeCheckSessionInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/CreateSubscribeCheckSessionInput.java new file mode 100644 index 0000000..55d45c7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/CreateSubscribeCheckSessionInput.java @@ -0,0 +1,28 @@ +package com.sonic.lion.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: mzc + * @date: 2023-05-12 10:31 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CreateSubscribeCheckSessionInput { + + @ApiModelProperty("订阅的商品ID") + private String subProductId; + + @ApiModelProperty("订阅支付成功跳转地址") + private String returnUrl; + + @ApiModelProperty("订阅支付失败跳转地址") + private String cancelUrl; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/FreezeInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/FreezeInput.java new file mode 100644 index 0000000..bae57ee --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/FreezeInput.java @@ -0,0 +1,29 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.domain.enums.FreezeType; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; + +/** + * @author: code + * @date: 2025/05/14 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@ToString +public class FreezeInput { + + private Long accountId; + + private String tradeNo; + + private PayChannel payChannel; + + private Long amount; + + private FreezeType freezeType; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/FundChangeInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/FundChangeInput.java new file mode 100644 index 0000000..296501e --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/FundChangeInput.java @@ -0,0 +1,61 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/10 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FundChangeInput { + + /** + * 平台代码 + */ + private String platform; + + /** + * 变动金额的用户id + */ + private Long accountId; + + /** + * 变动的金额 + */ + private Long amount; + + /** + * 变动金额的用户账户渠道 + */ + private PayChannel payChannel; + + /** + * 对手方账户 + */ + private String desAccountNo; + + /** + * 交易号 + */ + private String tradeNo; + + /** + * 业务订单号 + */ + private String bizNum; + + /** + * 交易类型 + */ + private BizType bizType; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/GetFormSchemaInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/GetFormSchemaInput.java new file mode 100644 index 0000000..9926810 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/GetFormSchemaInput.java @@ -0,0 +1,39 @@ +package com.sonic.lion.domain.input; + +import lombok.Data; + +/** + * @Author code + * @Description TODO + * @Date 2024/9/19 11:16 + * @Version 1.0 + */ +@Data +public class GetFormSchemaInput { + + /** + * 银行国家/地区代码(由 2 个字母组成的 ISO 3166-2 国家/地区代码) + */ + private String account_currency; + + /** + * 账户货币 (3 个字母的 ISO 4217 货币代码) + */ + private String bank_country_code; + + /** + * 付款方式,即 SWIFT、LOCAL + */ + private String entity_type = "LOCAL"; + + /** + * 本地付款的具体类型(子类型) + */ + private String local_clearing_system; + + /** + * 受益人的实体类型,即 COMPANY、PERSONAL + */ + private String payment_method = "PERSONAL"; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/IOSReceiptInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/IOSReceiptInput.java new file mode 100644 index 0000000..3aa038a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/IOSReceiptInput.java @@ -0,0 +1,37 @@ +package com.sonic.lion.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class IOSReceiptInput { + + private String productId; + private String transactionId; + private String originalTransactionId; + private Long purchase_date_ms; + private Long expires_date_ms; + + public String getSubscriptionId(){ + return originalTransactionId; + } + + public LocalDateTime getPurchaseDate(){ + LocalDateTime purchaseDate = LocalDateTime.ofEpochSecond(purchase_date_ms/1000, 0, ZoneOffset.ofHours(8)); + return purchaseDate; + } + + public LocalDateTime getExpiresDate(){ + LocalDateTime expiresDate = LocalDateTime.ofEpochSecond(expires_date_ms/1000, 0, ZoneOffset.ofHours(8)); + return expiresDate; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/MerchantSubscribeBo.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/MerchantSubscribeBo.java new file mode 100644 index 0000000..44e9ebb --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/MerchantSubscribeBo.java @@ -0,0 +1,43 @@ +package com.sonic.lion.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @Author mzc + * Date: 2022/4/26 11:17 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MerchantSubscribeBo { + + @ApiParam(value = "商家订阅价格档位") + private String priceType; + + @ApiParam(value = "商家订阅操作类型") + private String optType; + + @ApiParam(value = "商家订阅是否自动续订") + private Boolean isAutoRenew; + + @ApiModelProperty(value = "如果是订阅,则为订阅开始时间,如果是续订,则为上次的订阅的过期时间") + private LocalDateTime startTime; + + @ApiModelProperty("是否AI 订阅") + private Boolean isAiSubscribe; + + @ApiModelProperty("如果是AI 订阅,对应的商家用户id") + private Long merchantUserId; + + @ApiModelProperty("平台抽成金额") + private Long systemAmount; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PaymentBillInfo.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PaymentBillInfo.java new file mode 100644 index 0000000..d441ed7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PaymentBillInfo.java @@ -0,0 +1,93 @@ +package com.sonic.lion.domain.input; + +import com.paypal.api.payments.CountryCode; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Data +public class PaymentBillInfo { + + /** + * 客户ID + */ + private String customerId; + + /** + * 地址1 + */ + @NotBlank + private String addressLine1; + /** + * 地址2 + */ + @NotBlank + private String addressLine2; + /** + * 城市 + */ + @NotBlank + private String city; + /** + * 州 + */ + @NotBlank + private String state; + /** + * 邮编 + */ + @NotBlank + private String zip; + /** + * 国家代码(2位) + */ + @NotNull + private CountryCode countryCode; + /** + * 邮箱 + */ + @NotBlank + private String email; + /** + * 手机号 + */ + @NotBlank + private String phone; + + /** + * 手机区号 + */ + @NotBlank + private String phoneCountryCode; + + /** + * 姓名 + */ + @NotBlank + private String firstName; + + /** + * 姓名 + */ + @NotBlank + private String lastName; + + /** + * 用户注册时间 + */ + @NotBlank + private LocalDateTime registerTime; + + /** + * 校验airwallex渠道充值数据是否齐全 + * @return + */ + public Boolean airwallexCheckPass() { + //校验参数是否齐全(必须包含名字和邮箱) + return StringUtils.isNotEmpty(firstName) && StringUtils.isNotEmpty(lastName) && StringUtils.isNotEmpty(email); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PayssionCreateOrderReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PayssionCreateOrderReq.java new file mode 100644 index 0000000..fd969bf --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PayssionCreateOrderReq.java @@ -0,0 +1,86 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.utils.MD5Utils; +import lombok.Data; +import lombok.ToString; + +@ToString +@Data +public class PayssionCreateOrderReq { + private String api_key; + private String pm_id = "payssion_test"; + private String amount; + private String currency = "USD"; + private String description; + /** + * 必填 商户订单号,最长64字符 + */ + private String order_id; + /** + * api签名 + */ + private String api_sig; + + /** + * 支付返回后跳转的页面URL + */ + private String return_url; + + /** + * 客户邮箱 + */ + private String payer_email; + + /** + * 客户姓名 + */ + private String payer_name; + + public String getApi_sig(String secret_key) { + /** + * 1、将api_key, pm_id, amount, currency, order_id以及应用的secret_key字符串,以 “|”为分隔符串联成一个字符串 api_key|pm_id|amount|currency|order_id|secret_key + * 2、将第一步骤串联起来的的字符串经md5加密生成最终的api_sig(小写) 具体代码示例: + */ + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(api_key); + stringBuilder.append("|"); + stringBuilder.append(pm_id); + stringBuilder.append("|"); + stringBuilder.append(amount); + stringBuilder.append("|"); + stringBuilder.append(currency); + stringBuilder.append("|"); + stringBuilder.append(order_id); + stringBuilder.append("|"); + stringBuilder.append(secret_key); + return MD5Utils.stringToMD5(stringBuilder.toString()).toLowerCase(); + } + + public static void main(String[] args) { + PayssionCreateOrderReq req = new PayssionCreateOrderReq(); + req.setApi_key("sandbox_d7c4d8dc5e959e43"); + req.setAmount("1.00"); + req.setCurrency("USD"); + req.setOrder_id("aaa111111"); + String s = req.getApi_sigV2("hqconFULlNCmaBhdF0c61UZt7zHGKF21"); + System.out.println(s); + } + + public String getApi_sigV2(String secret_key) { + /** + * 1、将api_key, pm_id, amount, currency, order_id以及应用的secret_key字符串,以 “|”为分隔符串联成一个字符串 api_key|amount|currency|order_id|secret_key + * 2、将第一步骤串联起来的的字符串经md5加密生成最终的api_sig(小写) 具体代码示例: + */ + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(api_key); + stringBuilder.append("|"); + stringBuilder.append(amount); + stringBuilder.append("|"); + stringBuilder.append(currency); + stringBuilder.append("|"); + stringBuilder.append(order_id); + stringBuilder.append("|"); + stringBuilder.append(secret_key); + return MD5Utils.stringToMD5(stringBuilder.toString()).toLowerCase(); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PayssionGetOrderDetailReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PayssionGetOrderDetailReq.java new file mode 100644 index 0000000..fe364c9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PayssionGetOrderDetailReq.java @@ -0,0 +1,43 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.utils.MD5Utils; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Data +public class PayssionGetOrderDetailReq { + + + private String api_key; + + /** + * 交易id + */ + private String transaction_id ; + /** + * 订单号 + */ + private String order_id ; + + private String api_sig; + + + public String getApi_sig(String secret_key) { + /** + * 1、将api_key, transaction_id, order_id以及应用的sercret_key字符串,以 “|”为分隔符串联成一个字符串 api_key|transaction_id|order_id|secret_key + * 2、将第一步骤串联起来的的字符串经md5加密生成最终的notify_sig(小写) 具体代码示例: + */ + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(api_key); + stringBuilder.append("|"); +// stringBuilder.append(transaction_id); +// stringBuilder.append("|"); + stringBuilder.append(order_id); + stringBuilder.append("|"); + stringBuilder.append(secret_key); + log.info("原始字符串:{}",stringBuilder); + return MD5Utils.stringToMD5(stringBuilder.toString()).toLowerCase(); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PayssionNotify.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PayssionNotify.java new file mode 100644 index 0000000..e6e7af6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PayssionNotify.java @@ -0,0 +1,52 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.utils.MD5Utils; +import lombok.Data; +import lombok.ToString; + +@ToString +@Data +public class PayssionNotify { + /** + * 交易id + */ + private String pm_id; + + private String amount; + + private String currency; + /** + * 订单号 + */ + private String order_id; + + private String transaction_id; + + private String state; + + private String notify_sig; + + public boolean checkSig(String api_key, String secret_key) { + /** + * api_key|pm_id|amount|currency|order_id|state|secret_key + * 2、将第一步骤串联起来的的字符串经md5加密生成最终的notify_sig(小写) 具体代码示例: + */ + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(api_key); + stringBuilder.append("|"); + stringBuilder.append(pm_id); + stringBuilder.append("|"); + stringBuilder.append(amount); + stringBuilder.append("|"); + stringBuilder.append(currency); + stringBuilder.append("|"); + stringBuilder.append(order_id); + stringBuilder.append("|"); + stringBuilder.append(state); + stringBuilder.append("|"); + stringBuilder.append(secret_key); + String sign = MD5Utils.stringToMD5(stringBuilder.toString()).toLowerCase(); + return sign.equals(notify_sig); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PlatformGiftInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PlatformGiftInput.java new file mode 100644 index 0000000..cbe1373 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PlatformGiftInput.java @@ -0,0 +1,70 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.BizType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Data +public class PlatformGiftInput { + private String outTradeNo; + private String platform; + private String extend; + private Long uid; + private String name; + private LocalDateTime createTime; + private Long amount; //赠送金额 + private Type type; + + private String ip; + private String reason; + + + @AllArgsConstructor + @Getter + public enum Type { + + NEW_USER_GIFT(BizType.NEW_USER_GIFT, false, BuffType.BALANCE), //新手礼 + VIP_BUFF_GIFT(BizType.VIP_BUFF_GIFT, false, BuffType.BALANCE), //订阅VIP 赠送BUFF + SIGN_IN_GIFT(BizType.SIGN_IN_GIFT, false, BuffType.BALANCE), //签到 赠送BUFF + + ; + private BizType bizType; //业务类型 + private boolean needConfirm; //是否需要确认。 + private BuffType buffType; + } + + + @Getter + public enum BuffType { + /** + * 可消费Buff + */ + BALANCE(1, "BALANCE"), + /** + * 可提现收入 + */ + WITHDRAWABLE_INCOME(2, "WITHDRAWABLE_INCOME"), + /** + * 待入账收入 + */ + AWAITING_INCOME(3, "AWAITING_INCOME"), + /** + * 冻结收入 + */ + FROZEN_INCOME(4, "FROZEN_INCOME"), + FROZEN_BALANCE(5, "FROZEN_BALANCE"), + ; + + private final int value; + private final String desc; + + BuffType(int value, String desc) { + this.value = value; + this.desc = desc; + } + } + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PrePaymentInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PrePaymentInput.java new file mode 100644 index 0000000..a4ae77f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/PrePaymentInput.java @@ -0,0 +1,149 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.PaymentType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @author: code + * @date: 2025/05/18 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PrePaymentInput { + + /** + * 平台代码 + */ + private String platform; + + /** + * 外部交易号(如订单号) + */ + private String outTradeNo; + + /** + * 外部交易号关联单号(如:打赏单号关联的游戏订单的订单号) + */ + private String outTradeNoRelationNo; + + /** + * 业务类型 + */ + private BizType bizType; + + /** + * 交易标题或名称 + */ + private String name; + + /** + * 本方(发起方)账户(account .id) + */ + private Long srcAccountId; + + /** + * 收款方账号编号 + */ + private String desAccountNo; + + /** + * 收款方账号类型:参看DesAccountType + */ + private Integer desAccountType; + + /** + * 用户实得金额 + */ + private Long actualAmount; + + /** + * 商品金额,单位:分 + */ + private Long productAmount; + + /** + * 优惠金额,单位:分 + */ + private Long promoAmount; + + /** + * 备注 + */ + private String remark; + + private String srcAccountName; + + private String desAccountName; + + /** + * 支付渠道 + */ + private PayChannel payChannel; + + /** + * 实付金额 + */ + private Long occurAmount; + + /** + * 手续费 + */ + private Long fee; + + /** + * 平台费用 10% + */ + private Long platformFee; + + /** + * 三方费用 + */ + private Long thirdFee; + + /** + * 付款方式 + */ + private PaymentType paymentType; + + /** + * 充值并支付时,充值后付款的交易号 + */ + private String paymentTradeNo; + + /** + * 关闭交易时间 + */ + private LocalDateTime closeTime; + + /** + * 资源key + */ + private String resourceKey; + + /** + * 资源数量 + */ + private Integer resourceNum; + + /** + * 扩展信息 + */ + private String extend; + + private String ip; + + private String productId; + + private String clientVersion; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/RefundVipInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/RefundVipInput.java new file mode 100644 index 0000000..17730bf --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/RefundVipInput.java @@ -0,0 +1,77 @@ +package com.sonic.lion.domain.input; + +import lombok.Data; + +import java.time.Duration; +import java.time.LocalDateTime; + +@Data +public class RefundVipInput { + + /** + * 用户Id + */ + private Long userId; + + /** + * 会员类型 + */ + private String memberType; + + /** + * 赠送的会员类型 + */ + private String rewardMemberType; + + /** + * 到期时间 + */ + private LocalDateTime endTime; + + /** + * 赠送的到期时间 + */ + private LocalDateTime rewardEndTime; + + /** + * /** + * 1、endTime 未过期,endTime - 当前时间 = 会员剩余时间 (取memberType为会员类型) + * 2、rewardEndTime 未过期,endTime为空或已过期,rewardEndTime - 当前时间 = 会员剩余时间(取rewardMemberType为会员类型) + * 3、rewardEndTime未过期,endTime未过期,rewardEndTime - endTime = 会员剩余时间(取rewardMemberType为会员类型) + */ + + + /** + * 订阅应该退的 + * + * @return + */ + public long secondsWhenEndTimeNotExp() { + LocalDateTime now = LocalDateTime.now(); + if (endTime != null && endTime.isAfter(now)) { + Duration duration = Duration.between(now, endTime); + return duration.getSeconds(); + } + return 0; + } + + /** + * 赠送 应该退的 + * + * @return + */ + public long secondsWhenRewardEndTimeNotExp() { + LocalDateTime now = LocalDateTime.now(); + if (rewardEndTime != null && rewardEndTime.isAfter(now)) { + if (endTime != null && endTime.isAfter(now)) { + Duration duration = Duration.between(endTime, rewardEndTime); + return duration.getSeconds(); + } + if (endTime == null || endTime.isBefore(now)) { + Duration duration = Duration.between(now, rewardEndTime); + return duration.getSeconds(); + } + } + return 0; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/SubChargeProductListInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/SubChargeProductListInput.java new file mode 100644 index 0000000..48264e7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/SubChargeProductListInput.java @@ -0,0 +1,29 @@ +package com.sonic.lion.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * @description: + * @author: mzc + * @date: 2023-11-23 16:58 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class SubChargeProductListInput { + + @ApiModelProperty("平台(web、android、iOS)") + @NotBlank + private String platform; + + @ApiModelProperty("版本号(1)") + private String version; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/TradeNotifyInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/TradeNotifyInput.java new file mode 100644 index 0000000..31c62bb --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/TradeNotifyInput.java @@ -0,0 +1,26 @@ +package com.sonic.lion.domain.input; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/05/18 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TradeNotifyInput { + + private String channelSn; + + private Object body; + + private PayChannel payChannel; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/UnbindInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/UnbindInput.java new file mode 100644 index 0000000..638461f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/UnbindInput.java @@ -0,0 +1,18 @@ +package com.sonic.lion.domain.input; + +import io.swagger.annotations.ApiParam; +import lombok.Data; + +/** + * @Author code + * @Description TODO + * @Date 2024/9/23 10:05 + * @Version 1.0 + */ +@Data +public class UnbindInput { + + @ApiParam(value = "验证码", required = true) + private String checkCode; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/UploadReceiptInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/UploadReceiptInput.java new file mode 100644 index 0000000..5d74771 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/UploadReceiptInput.java @@ -0,0 +1,25 @@ +package com.sonic.lion.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UploadReceiptInput { + /** + * google 收据 token + */ + @NotBlank + private String receipt; + + /** + * 充值的产品ID + */ + private String productId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/WithdrawFeeInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/WithdrawFeeInput.java new file mode 100644 index 0000000..9cbb282 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/input/WithdrawFeeInput.java @@ -0,0 +1,34 @@ +package com.sonic.lion.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author code + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class WithdrawFeeInput { + /** + * 如果免手续费 , 免手续费的流水id + */ + private Long freeWithdrawBillId; + + /** + * 最终的手续费 + */ + private Long withdrawFee; + + + /** + * 平台费用 10% + */ + private Long platformFee; + + /** + * 三方费用 + */ + private Long thirdFee; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/AvailableChannelOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/AvailableChannelOutput.java new file mode 100644 index 0000000..d1be522 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/AvailableChannelOutput.java @@ -0,0 +1,40 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * @author: code + * @date: 2025/07/27 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AvailableChannelOutput { + + private List channelList; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Channel { + + private PayChannel payChannel; + + private String name; + + private Long paymentFeeBase; + + private BigDecimal paymentFeeRate; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelBindBankAccountOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelBindBankAccountOutput.java new file mode 100644 index 0000000..c894e29 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelBindBankAccountOutput.java @@ -0,0 +1,23 @@ +package com.sonic.lion.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/05 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelBindBankAccountOutput { + + private String openId; + + private String result; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelBindBankCardOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelBindBankCardOutput.java new file mode 100644 index 0000000..5753536 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelBindBankCardOutput.java @@ -0,0 +1,23 @@ +package com.sonic.lion.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/05 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelBindBankCardOutput { + + private String openId; + + private String result; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelBindPaypalOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelBindPaypalOutput.java new file mode 100644 index 0000000..6d81851 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelBindPaypalOutput.java @@ -0,0 +1,23 @@ +package com.sonic.lion.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/05 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelBindPaypalOutput { + + private String openId; + + private String result; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelCancelOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelCancelOutput.java new file mode 100644 index 0000000..0ce67e5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelCancelOutput.java @@ -0,0 +1,24 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.domain.enums.CallChannelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/03 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelCancelOutput { + + private CallChannelStatus status; + + private String result; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelCheckOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelCheckOutput.java new file mode 100644 index 0000000..0ba6ea0 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelCheckOutput.java @@ -0,0 +1,53 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.domain.enums.CallChannelStatus; +import com.sonic.lion.domain.enums.TradeEvent; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; + +/** + * @author: code + * @date: 2025/05/14 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelCheckOutput { + + private String batchId; + + private String transactionId; + + private TradeEvent channelEvent; + + private CallChannelStatus status; + + private String result; + + private String errorMessage; + + private String statusCode; + + private Map extendInfo; + + /** + * 支付的payerId 是否 在黑名单里面 + */ + private boolean inBlackList; + + public CallChannelStatus getStatus() { + //如果被标记为 已经成功了 需要判断 是否有 transactionId 必须同时存在才是真正成功 + if (StringUtils.isEmpty(transactionId) && CallChannelStatus.SUCC.equals(status)) { + return CallChannelStatus.PROCESSING; + } + return status; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelConfirmTransferOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelConfirmTransferOutput.java new file mode 100644 index 0000000..7a78c42 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelConfirmTransferOutput.java @@ -0,0 +1,26 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.domain.enums.CallChannelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/11 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelConfirmTransferOutput { + + private String transactionId; + + private CallChannelStatus status; + + private String result; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelCreateCustomerOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelCreateCustomerOutput.java new file mode 100644 index 0000000..9e33324 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelCreateCustomerOutput.java @@ -0,0 +1,23 @@ +package com.sonic.lion.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelCreateCustomerOutput { + + private String openId; + + private String result; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelPaymentOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelPaymentOutput.java new file mode 100644 index 0000000..35846fe --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelPaymentOutput.java @@ -0,0 +1,45 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.domain.enums.CallChannelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * @author: code + * @date: 2025/05/18 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelPaymentOutput { + + private CallChannelStatus status; + + private String submitId; + + private String batchId; + + private String transactionId; + + private String paymentUrl; + + private String result; + + /** + * 客户端密钥(airwallex支付使用) + */ + private String clientSecret; + + /** + * 扩展信息 + */ + private Map extendInfo; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelPayoutOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelPayoutOutput.java new file mode 100644 index 0000000..7bc8d17 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelPayoutOutput.java @@ -0,0 +1,54 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.domain.enums.CallChannelStatus; +import lombok.*; + +/** + * @author: code + * @date: 2025/05/08 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class ChannelPayoutOutput { + + /** + * 请求ID + */ + private String batchId; + + + /** + * 交易id + */ + private String transactionId; + + /** + * 渠道状态 + */ + private CallChannelStatus status; + + /** + * 返回的完整结果 + */ + private String result; + + /** + * 错误消息 + */ + private String errorMessage; + + /** + * 状态码 + */ + private String statusCode; + + /** + * 渠道调用id + */ + private Long payCallChannelRecordId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelRefundOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelRefundOutput.java new file mode 100644 index 0000000..b9fa9b5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelRefundOutput.java @@ -0,0 +1,28 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.domain.enums.CallChannelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/03 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelRefundOutput { + + private String batchId; + + private String transactionId; + + private CallChannelStatus status; + + private String result; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelTransferOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelTransferOutput.java new file mode 100644 index 0000000..5558a16 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelTransferOutput.java @@ -0,0 +1,23 @@ +package com.sonic.lion.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/06/11 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelTransferOutput { + + private String transactionId; + + private String result; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelWebhookOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelWebhookOutput.java new file mode 100644 index 0000000..fab89a9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChannelWebhookOutput.java @@ -0,0 +1,40 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.domain.enums.CallChannelStatus; +import com.sonic.lion.domain.enums.TradeEvent; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelWebhookOutput { + + private String batchId; + + private String transactionId; + + private CallChannelStatus status; + + private TradeEvent channelEvent; + + private String result; + + private String eventType; + + private Map extendInfo; + + private String errorMessage; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChargeProductConfigListOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChargeProductConfigListOutput.java new file mode 100644 index 0000000..2031a81 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChargeProductConfigListOutput.java @@ -0,0 +1,38 @@ +package com.sonic.lion.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChargeProductConfigListOutput { + + /** + * 商品ID + */ + private String productId; + + /** + * 充值到账的BUFF金额 + */ + private Long chargeAmount; + + /** + * 三方渠道的实付金额 + */ + private Long payAmount; + + /** + * 赠送的总金额 + */ + private Long giftAmount; + + /** + * 是否热门标记 + */ + private Boolean hot; +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChargeProductConfigOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChargeProductConfigOutput.java new file mode 100644 index 0000000..660b4d9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/ChargeProductConfigOutput.java @@ -0,0 +1,32 @@ +package com.sonic.lion.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @Author code + * @Description 充值档位配置出参 + * @Date 2024/5/27 17:00 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChargeProductConfigOutput { + + /** + * 倒计时剩余时间 + */ + private Long countdown; + + /** + * 充值档位(商品)列表 + */ + private List productList; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/CreateBeneficiaryOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/CreateBeneficiaryOutput.java new file mode 100644 index 0000000..b3b9f93 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/CreateBeneficiaryOutput.java @@ -0,0 +1,54 @@ +package com.sonic.lion.domain.output; + +import lombok.Data; + +/** + * @Author code + * @Description TODO + * @Date 2024/9/19 17:32 + * @Version 1.0 + */ +@Data +public class CreateBeneficiaryOutput { + + /** + * 受益人ID + */ + private String beneficiaryId; + + /** + * 货币单位 + */ + private String accountCurrency; + + /** + * 收款人姓名 + */ + private String accountName; + + /** + * 收款人卡号 + */ + private String accountNumber; + + /** + * 银行的国家代码 + */ + private String bankCountryCode; + + /** + * 银行名称 + */ + private String bankName; + + /** + * 具体的本地付款类型 + */ + private String localClearingSystem; + + /** + * 接收账单流水的邮箱 + */ + private String email; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/CreateSubscribeCheckoutSessionOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/CreateSubscribeCheckoutSessionOutput.java new file mode 100644 index 0000000..7e0c7bf --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/CreateSubscribeCheckoutSessionOutput.java @@ -0,0 +1,20 @@ +package com.sonic.lion.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * stripe订阅付款链接 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CreateSubscribeCheckoutSessionOutput { + + @ApiModelProperty("支付url") + private String payUrl; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/EnabledPayChannelOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/EnabledPayChannelOutput.java new file mode 100644 index 0000000..d6538fe --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/EnabledPayChannelOutput.java @@ -0,0 +1,14 @@ +package com.sonic.lion.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class EnabledPayChannelOutput { + + @ApiModelProperty("可用的渠道列表") + private List channels; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/FilterTypeDict.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/FilterTypeDict.java new file mode 100644 index 0000000..58ab2ec --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/FilterTypeDict.java @@ -0,0 +1,25 @@ +package com.sonic.lion.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: 获取筛选中Buff Balance Income 类型列表 + * @author: mzc + * @date: 2023-11-23 19:08 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class FilterTypeDict { + + @ApiModelProperty("类型枚举") + private String code; + + @ApiModelProperty("类型名称") + private String name; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/FilterTypeListOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/FilterTypeListOutput.java new file mode 100644 index 0000000..abf554f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/FilterTypeListOutput.java @@ -0,0 +1,27 @@ +package com.sonic.lion.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: 获取筛选中Buff Balance Income 类型列表 + * @author: mzc + * @date: 2023-11-23 19:08 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class FilterTypeListOutput { + + @ApiModelProperty("Buff Balance类型列表") + private List buffBalanceTypeList; + + @ApiModelProperty("Buff Income类型列表") + private List buffIncomeTypeList; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/MemberDetailOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/MemberDetailOutput.java new file mode 100644 index 0000000..3e6f369 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/MemberDetailOutput.java @@ -0,0 +1,29 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.domain.MemberPrivDict; +import com.sonic.lion.domain.entity.UserSubscription; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-09-16 14:14 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class MemberDetailOutput { + + @ApiModelProperty("用户会员信息") + private UserSubscription userMemberInfo; + + @ApiModelProperty("用户会员权限列表") + private List memberPrivList; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/PayPalInfoOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/PayPalInfoOutput.java new file mode 100644 index 0000000..74fc2ce --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/PayPalInfoOutput.java @@ -0,0 +1,24 @@ +package com.sonic.lion.domain.output; + +import lombok.Builder; +import lombok.Data; + +/** + * @author: code + * @date: 2025/07/21 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +public class PayPalInfoOutput { + + private String userId; + private String name; + private String email; + private Boolean emailVerified; + private String phoneNumber; + private Boolean verifiedAccount; + private String accountType; + private String payerId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/PrePaymentOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/PrePaymentOutput.java new file mode 100644 index 0000000..375db64 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/PrePaymentOutput.java @@ -0,0 +1,34 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.enums.TradeStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/05/18 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PrePaymentOutput { + + private String tradeNo; + + /** + * 外部交易号(如订单号) + */ + private String outTradeNo; + + /** + * 外部交易号关联单号(如:打赏单号关联的游戏订单的订单号) + */ + private String outTradeNoRelationNo; + + private TradeStatus tradeStatus; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/SubProductListOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/SubProductListOutput.java new file mode 100644 index 0000000..92be849 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/SubProductListOutput.java @@ -0,0 +1,48 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.domain.entity.AppStoreProduct; +import com.sonic.lion.domain.enums.ProductType; +import com.sonic.lion.enums.MemberType; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubProductListOutput { + + @ApiModelProperty("平台") + private String platform; + + @ApiModelProperty("产品类型 连续购买订阅 和 非连续") + private ProductType productType; + + @ApiModelProperty("订阅套餐类型") + private MemberType memberType; + + @ApiModelProperty("订阅时长") + private AppStoreProduct.PERIOD period; + + @ApiModelProperty("应用id") + private String bundleId; + + @ApiModelProperty("内购商品项id") + private String productId; + + @ApiModelProperty("优惠多少 百分比") + private String discount; + + @ApiModelProperty("得到的Buff") + private Long chargeAmount; + + @ApiModelProperty("支付金额") + private Long payAmount; + + @ApiModelProperty("免费天数") + private Integer freeDays; + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/SyncChannelOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/SyncChannelOutput.java new file mode 100644 index 0000000..b018a7c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/SyncChannelOutput.java @@ -0,0 +1,12 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.domain.enums.CallChannelStatus; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class SyncChannelOutput { + private CallChannelStatus callChannelStatus; + private String message; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/TradeHandleOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/TradeHandleOutput.java new file mode 100644 index 0000000..e69c1d1 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/TradeHandleOutput.java @@ -0,0 +1,64 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PaymentType; +import com.sonic.lion.enums.TradeStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @author: code + * @date: 2025/06/12 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TradeHandleOutput { + + private Long uid; + + protected String tradeNo; + + protected String outTradeNo; + + private BizType bizType; + + protected Long amount; + + private TradeStatus status; + + private LocalDateTime statusInTime; + + private PaymentType paymentType; + + /** + * 交易实际成交金额,单位:分 + */ + private Long occurAmount; + + /** + * 优惠金额 + */ + private Long promoAmount; + + /** + * 优惠金额 + */ + private Long fee; + + private String tradeMessage; + + + private String paymentTradeNo; + private String paymentTradeOrderNo; + private BizType paymentBizType; + private Long chargeAmount; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/WalletOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/WalletOutput.java new file mode 100644 index 0000000..d841676 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/WalletOutput.java @@ -0,0 +1,51 @@ +package com.sonic.lion.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * @author: code + * @date: 2025/06/23 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WalletOutput { + + /** + * 现页面上显示的Balance为可用于付款的金额,实际为后加上的charge字段(用户充值金额) + */ + private Long balance = 0L; + + /** + * 现页面上显示的Income为用户收入金额,实际为之前定义的balance等字段 + */ + private Long income = 0L; + + /** + * 可提现金额(income的一部分) + */ + private Long withdrawable = 0L; + + /** + * 申请提现处理中的金额 + */ + private Long requestWithdraw = 0L; + + /** + * 待入账收入 + */ + private Long awaitingIncome = 0L; + + /** + * 提现手续费比例 + */ + private BigDecimal wdFeeRate = new BigDecimal(0L); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/WithdrawFeeReduceInfoOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/WithdrawFeeReduceInfoOutput.java new file mode 100644 index 0000000..3c31dbc --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/output/WithdrawFeeReduceInfoOutput.java @@ -0,0 +1,34 @@ +package com.sonic.lion.domain.output; + +import com.sonic.lion.enums.FreeWithdrawReason; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * @description: + * @author: mzc + * @date: 2023-07-25 17:34 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class WithdrawFeeReduceInfoOutput { + + @ApiModelProperty("是否提现减免") + private Boolean isWithdrawFeeReduce = false; + + @ApiModelProperty("减免原因") + private FreeWithdrawReason freeWithdrawReason; + + @ApiModelProperty("减免比例") + private BigDecimal reduceRate; + + @ApiModelProperty("减免提示文本") + private String tip; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/AppProductInfoInput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/AppProductInfoInput.java new file mode 100644 index 0000000..301496b --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/AppProductInfoInput.java @@ -0,0 +1,11 @@ +package com.sonic.lion.domain.req; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class AppProductInfoInput { + + @ApiModelProperty("商品ID") + private String appProductId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BillDetailReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BillDetailReq.java new file mode 100644 index 0000000..837d8b9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BillDetailReq.java @@ -0,0 +1,24 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @author: code + * @date: 2025/06/23 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BillDetailReq { + + @NotNull + private Long id; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BillListReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BillListReq.java new file mode 100644 index 0000000..517703a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BillListReq.java @@ -0,0 +1,53 @@ +package com.sonic.lion.domain.req; + +import com.sonic.common.rpc.Page; +import com.sonic.lion.domain.entity.AccountBuffBill; +import com.google.common.collect.ImmutableMap; +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import java.util.Map; + +/** + * @author: code + * @date: 2025/06/23 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BillListReq { + + private Type type; + + @Builder.Default + Map query = ImmutableMap.of(); + + @Builder.Default + Page page = new Page<>(1, 10); + + public Map getQuery() { + return query == null ? ImmutableMap.of() : query; + } + + public Page getPage() { + return page == null ? new Page<>(1, 10) : page; + } + + @Getter + public enum Type { + + BALANCE, + + INCOME, + ; + } + + @ApiModelProperty("开始时间戳") + private Long startTime; + + @ApiModelProperty("结束时间戳") + private Long endTime; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindBankAccountReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindBankAccountReq.java new file mode 100644 index 0000000..7c4ec55 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindBankAccountReq.java @@ -0,0 +1,32 @@ +package com.sonic.lion.domain.req; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BindBankAccountReq { + + @NotNull + private PayChannel payChannel; + + @NotBlank + private String transferMethodCountry; + + @NotBlank + private String email; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindBankCardReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindBankCardReq.java new file mode 100644 index 0000000..98fb987 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindBankCardReq.java @@ -0,0 +1,36 @@ +package com.sonic.lion.domain.req; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BindBankCardReq { + + @NotNull + private PayChannel payChannel; + + @NotBlank + private String cardNumber; + + @NotNull + private Date dateOfExpiry; + + @NotBlank + private String cvv; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindPaypalAccountReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindPaypalAccountReq.java new file mode 100644 index 0000000..4e8b8af --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindPaypalAccountReq.java @@ -0,0 +1,32 @@ +package com.sonic.lion.domain.req; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author: code + * @date: 2025/06/04 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BindPaypalAccountReq { + + @NotNull + private PayChannel payChannel; + + @NotBlank + private String transferMethodCountry; + + @NotBlank + private String email; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindSubscibeReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindSubscibeReq.java new file mode 100644 index 0000000..183db61 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BindSubscibeReq.java @@ -0,0 +1,15 @@ +package com.sonic.lion.domain.req; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class BindSubscibeReq { + + @NotBlank + private String subscriptionId; + + @NotBlank + private String productId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BuffAwaitingFronzeReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BuffAwaitingFronzeReq.java new file mode 100644 index 0000000..1148627 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/BuffAwaitingFronzeReq.java @@ -0,0 +1,15 @@ +package com.sonic.lion.domain.req; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Data +public class BuffAwaitingFronzeReq { + /** + * 交易订单列表 + */ + @NotEmpty(message = "tradeNoList can not be empty") + private List tradeNoList; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/ChargeMonthRewardReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/ChargeMonthRewardReq.java new file mode 100644 index 0000000..52d180f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/ChargeMonthRewardReq.java @@ -0,0 +1,11 @@ +package com.sonic.lion.domain.req; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class ChargeMonthRewardReq { + + @ApiModelProperty("档位Id") + private Long rewardId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CheckoutReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CheckoutReq.java new file mode 100644 index 0000000..6ccd5a2 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CheckoutReq.java @@ -0,0 +1,37 @@ +package com.sonic.lion.domain.req; + +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.PaymentType; +import com.sonic.lion.domain.input.PaymentBillInfo; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * @author: code + * @date: 2025/05/20 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CheckoutReq { + + @NotBlank + private String tradeNo; + + private PayChannel payChannel; + + private PaymentType paymentType = PaymentType.CHANNEL; + + private String returnUrl; + + private String cancelUrl; + +} + diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CloseTradeByResourceKeyReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CloseTradeByResourceKeyReq.java new file mode 100644 index 0000000..8cbd7a8 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CloseTradeByResourceKeyReq.java @@ -0,0 +1,24 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * @author: code + * @date: 2025/08/06 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CloseTradeByResourceKeyReq { + + @NotBlank + private String resourceKey; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CloseTradeReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CloseTradeReq.java new file mode 100644 index 0000000..6809a60 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CloseTradeReq.java @@ -0,0 +1,27 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * @author: code + * @date: 2025/06/12 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CloseTradeReq { + + @NotBlank + private String platform; + + @NotBlank + private String outTradeNo; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CompleteSecuredTradeReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CompleteSecuredTradeReq.java new file mode 100644 index 0000000..4188e06 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/CompleteSecuredTradeReq.java @@ -0,0 +1,27 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * @author: code + * @date: 2025/06/10 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CompleteSecuredTradeReq { + + @NotBlank + private String platform; + + @NotBlank + private String outTradeNo; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/EpalRefundReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/EpalRefundReq.java new file mode 100644 index 0000000..d5d2f74 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/EpalRefundReq.java @@ -0,0 +1,13 @@ +package com.sonic.lion.domain.req; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Data +public class EpalRefundReq { + + @NotEmpty + private List outTradeNoList; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/FreeWithdrawReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/FreeWithdrawReq.java new file mode 100644 index 0000000..151c169 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/FreeWithdrawReq.java @@ -0,0 +1,43 @@ +package com.sonic.lion.domain.req; + +import com.sonic.lion.enums.FreeWithdrawReason; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class FreeWithdrawReq { + + /** + * 用户ID + */ + private Long uid; + /** + * 开始时间 + */ + private LocalDateTime startTime; + + /** + * 结束时间 + */ + private LocalDateTime endTime; + + /** + * 提现手续费减免比例 + */ + private BigDecimal rate; + + /** + * 获得原因 + */ + private FreeWithdrawReason reason; + + private Boolean isDelete; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/GoogleUploadReceiptReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/GoogleUploadReceiptReq.java new file mode 100644 index 0000000..c8116b9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/GoogleUploadReceiptReq.java @@ -0,0 +1,52 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; + + + +/** + * 上传google Token校验 + * @author: code + * @date: 2025/06/22 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GoogleUploadReceiptReq { + + private Long receiptId; + + /** + * google 收据 token + */ + @NotBlank + private String receipt; + + /** + * 充值的产品ID + */ + private String productId; + + /** + * google的交易ID + */ + private String transactionId; + + /** + * 交易编号 + */ + @NotEmpty + private String tradeNo; + + private String key; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/GoogleUploadReceiptReqV2.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/GoogleUploadReceiptReqV2.java new file mode 100644 index 0000000..8bbd7dc --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/GoogleUploadReceiptReqV2.java @@ -0,0 +1,20 @@ +package com.sonic.lion.domain.req; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class GoogleUploadReceiptReqV2 { + + + @NotBlank + private String receipt; + + /** + * 充值的产品ID + */ + @NotBlank + private String productId; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/IapUploadReceiptReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/IapUploadReceiptReq.java new file mode 100644 index 0000000..bb5b247 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/IapUploadReceiptReq.java @@ -0,0 +1,42 @@ +package com.sonic.lion.domain.req; + +import com.alibaba.fastjson.JSONObject; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author: code + * @date: 2025/06/22 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IapUploadReceiptReq { + + private Long receiptId; + + /** + * 交易 + */ + @NotNull + private JSONObject transactions; + + /** + * iap收据 + */ + @NotBlank + private String receipt; + + /** + * 签名 + */ + private String signature; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/NotifyReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/NotifyReq.java new file mode 100644 index 0000000..b186895 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/NotifyReq.java @@ -0,0 +1,26 @@ +package com.sonic.lion.domain.req; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/05/18 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NotifyReq { + + private PayChannel payChannel; + + private String channelSn; + + private Object body; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/PaypalAutoCancelReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/PaypalAutoCancelReq.java new file mode 100644 index 0000000..0d011b0 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/PaypalAutoCancelReq.java @@ -0,0 +1,8 @@ +package com.sonic.lion.domain.req; + +import lombok.Data; + +@Data +public class PaypalAutoCancelReq { + private Long userId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/PreChargeReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/PreChargeReq.java new file mode 100644 index 0000000..78b013c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/PreChargeReq.java @@ -0,0 +1,24 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * @author: code + * @date: 2025/06/22 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PreChargeReq { + + private String productId; + + private String version; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/PrePaymentReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/PrePaymentReq.java new file mode 100644 index 0000000..8aa86c9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/PrePaymentReq.java @@ -0,0 +1,113 @@ +package com.sonic.lion.domain.req; + +import com.sonic.lion.enums.BizType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * @author: code + * @date: 2025/05/18 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PrePaymentReq { + + /** + * 平台代码(Balance 钱包余额) + */ + @NotBlank + private String platform = "Balance"; + + /** + * 平台抽成费用 + */ + private Long platformFee; + + /** + * 外部交易号(如订单号) + */ + @NotBlank + private String outTradeNo; + + /** + * 外部交易号关联单号 + */ + private String outTradeNoRelationNo; + + /** + * 业务类型 + */ + @NotNull + private BizType bizType; + + /** + * 交易标题或名称 + */ + @NotBlank + private String name; + + /** + * 本方(发起方)账户(account id) + */ + @NotNull + private Long srcAccountId; + + /** + * 对方账户 + */ + private Long desAccountId; + + /** + * 商品金额,单位:分 + */ + @Min(1) + @NotNull + private Long productAmount; + + /** + * 优惠金额,单位:分 + */ + @NotNull + private Long promoAmount; + + /** + * 备注 + */ + private String remark; + + /** + * 交易关闭时间 + */ + private LocalDateTime closeTime; + + /** + * 资源key + */ + private String resourceKey; + + /** + * 资源数量 + */ + private Integer resourceNum; + + /** + * 扩展信息 + */ + private String extend; + + /** + * ip 地址 + */ + private String ip; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/QueryReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/QueryReq.java new file mode 100644 index 0000000..9dc50af --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/QueryReq.java @@ -0,0 +1,33 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * @author: code + * @date: 2025/05/19 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QueryReq { + + /** + * 平台代码 + */ + @NotBlank + private String platform; + + /** + * 外部交易号(如订单号) + */ + @NotBlank + private String outTradeNo; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/ReasonReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/ReasonReq.java new file mode 100644 index 0000000..bef6b93 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/ReasonReq.java @@ -0,0 +1,12 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ReasonReq { + private String reason; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundAmountReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundAmountReq.java new file mode 100644 index 0000000..7cfc5fc --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundAmountReq.java @@ -0,0 +1,44 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author: code + * @date: 2025/06/03 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RefundAmountReq { + + /** + * 外部订单号 + */ + @NotBlank + private String outTradeNo; + + /** + * 退款金额 + */ + @NotNull + private Long amount; + + /** + * 是否要退优惠(平台发放的优惠券、优惠码的金额,这部分是平台补贴给商家的) + */ + private Boolean refundPromo; + + /** + * 是否全额退款 + */ + private Boolean fullRefund; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundQueryReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundQueryReq.java new file mode 100644 index 0000000..09a8436 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundQueryReq.java @@ -0,0 +1,35 @@ +package com.sonic.lion.domain.req; + +import com.sonic.common.rpc.Page; +import com.sonic.lion.domain.entity.AppleRefundRecord; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RefundQueryReq { + + @Builder.Default + Page page = new Page<>(1, 20); + + @ApiModelProperty("开始时间戳") + private LocalDateTime startTime; + + @ApiModelProperty("结束时间戳") + private LocalDateTime endTime; + + @ApiModelProperty("订单编号") + private String tradeNo; + + @ApiModelProperty("用户Id") + private Long userId; + + private String transactionId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundReq.java new file mode 100644 index 0000000..c314c9b --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundReq.java @@ -0,0 +1,29 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author: code + * @date: 2025/06/03 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RefundReq { + + @NotBlank + private String platform; + + @NotNull + private List outTradeNoList; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundReqV2.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundReqV2.java new file mode 100644 index 0000000..395f5ea --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RefundReqV2.java @@ -0,0 +1,35 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author: code + * @date: 2025/06/03 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RefundReqV2 { + + @NotBlank + private String platform; + + private RefundType refundType; + + @NotNull + private List outTradeNoRefundList; + + public enum RefundType{ + GAME, MERCHANT_SUBSCRIBE + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RevisePlanReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RevisePlanReq.java new file mode 100644 index 0000000..8c5c5d5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/RevisePlanReq.java @@ -0,0 +1,28 @@ +package com.sonic.lion.domain.req; + +import com.paypal.http.annotations.Model; +import com.paypal.http.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author code + * 变更订阅计划的 参数 + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +@Model +public class RevisePlanReq { + @SerializedName("plan_id") + private String plan_id; + + public String getPlan_id() { + return plan_id; + } + + public void setPlan_id(String plan_id) { + this.plan_id = plan_id; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/SearchChannelReportReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/SearchChannelReportReq.java new file mode 100644 index 0000000..bc67ce9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/SearchChannelReportReq.java @@ -0,0 +1,23 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/05/20 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchChannelReportReq { + + + private Integer payChannel; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/TradeQueryReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/TradeQueryReq.java new file mode 100644 index 0000000..4de971f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/TradeQueryReq.java @@ -0,0 +1,24 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * @author: code + * @date: 2025/05/20 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TradeQueryReq { + + @NotBlank + private Long submitId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/UploadReceiptReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/UploadReceiptReq.java new file mode 100644 index 0000000..7c14335 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/UploadReceiptReq.java @@ -0,0 +1,25 @@ +package com.sonic.lion.domain.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UploadReceiptReq { + /** + * google 收据 token + */ + @NotBlank + private String receipt; + + /** + * 充值的产品ID gg.epal.buff_0001 + */ + private String productId; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/WithdrawReq.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/WithdrawReq.java new file mode 100644 index 0000000..e50a1c2 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/req/WithdrawReq.java @@ -0,0 +1,70 @@ +package com.sonic.lion.domain.req; + +import com.sonic.lion.enums.BindDataType; +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author: code + * @date: 2025/05/11 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WithdrawReq { + + /** + * 平台代码 + */ + @NotBlank + private String platform; + + /** + * 外部交易号(如订单号) + */ + @NotBlank + private String outTradeNo; + + /** + * 交易标题或名称 + */ + @NotBlank + private String name; + + /** + * 本方(发起方)账户(account .id) + */ + @NotNull + private Long uid; + + /** + * 渠道ID + */ + @NotNull + private PayChannel payChannel; + + /** + * 交易金额,单位:分 + */ + @NotNull + private Long amount; + + /** + * 备注 + */ + private String remark; + + /** + * payChannel为braintree等聚合支付渠道时,转账需要选择所绑定的具体账户类型 + */ + private BindDataType bindDataType; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/AppProductInfoOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/AppProductInfoOutput.java new file mode 100644 index 0000000..250e55a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/AppProductInfoOutput.java @@ -0,0 +1,26 @@ +package com.sonic.lion.domain.resp; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class AppProductInfoOutput { + + @ApiModelProperty("商品ID") + private String productId; + + @ApiModelProperty("会员类型:VIP") + private String memberType; + + @ApiModelProperty("订阅类型:SUB_MONTH、SUB_SEASON、SUB_YEAR") + private String period; + + @ApiModelProperty("实际到账金额") + private Long chargeAmount; + + @ApiModelProperty("实际支付金额") + private Long payAmount; + + @ApiModelProperty("免费天数") + private Integer freeDays; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/BillOutput.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/BillOutput.java new file mode 100644 index 0000000..2788039 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/BillOutput.java @@ -0,0 +1,83 @@ +package com.sonic.lion.domain.resp; + +import com.sonic.lion.enums.BizType; +import com.sonic.lion.domain.enums.BuffType; +import com.sonic.lion.domain.enums.InOrOut; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @author: code + * @date: 2025/06/23 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BillOutput { + + private Long id; + + private String nickname; + + private String headImg; + + private Long amount; + + private String item; + + private LocalDateTime time; + + private String payment; + + private InOrOut inOrOut; + + private BizType bizType; + + private String tradeNo; + + private String bizNum; + + private String platform; + + private Long desUid; + + @ApiModelProperty("desUid对应用户自定义ID") + private String custId; + + + private String status; + + private String message; + + private String extend; + + private Integer giftId; + + /** + * buff类型 + */ + private BuffType buffType; + + /** + * 转入可提现收入时间 + */ + private LocalDateTime toWithdrawableIncomeTime; + /** + * 订阅类型 待入账会按月分期到期,期数 + */ + private Integer period; + + /** + * 订阅类型 待入账会按月分期到期 每期金额 + */ + private Long periodAmount; +} + diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/BuffCheckoutResp.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/BuffCheckoutResp.java new file mode 100644 index 0000000..5e5d2b6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/BuffCheckoutResp.java @@ -0,0 +1,26 @@ +package com.sonic.lion.domain.resp; + +import com.sonic.lion.enums.TradeStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/05/20 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BuffCheckoutResp { + + private String paymentUrl; + + private TradeStatus tradeStatus; + + private Long fee; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/ChargeFeeResp.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/ChargeFeeResp.java new file mode 100644 index 0000000..c964d05 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/ChargeFeeResp.java @@ -0,0 +1,21 @@ +package com.sonic.lion.domain.resp; + +import com.sonic.lion.enums.PayChannel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ChargeFeeResp { + /** + * 支付渠道 + */ + private PayChannel payChannel; + + /** + * 充值手續費 + */ + private Long chargeFee; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/ChargeMonthReward.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/ChargeMonthReward.java new file mode 100644 index 0000000..a316641 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/ChargeMonthReward.java @@ -0,0 +1,44 @@ +package com.sonic.lion.domain.resp; + +import com.sonic.lion.enums.I18nResources; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.text.MessageFormat; + +@Data +public class ChargeMonthReward { + + @ApiModelProperty("当月累计充值金额buff") + private Long totalAmount; + + @ApiModelProperty("要领取奖品最少要达到多少BUFF") + private Long minBuff; + + @ApiModelProperty("是否已经领取") + private Boolean receive; + + @ApiModelProperty("奖品档位Id") + private Long rewardId; + + @ApiModelProperty("赠送多少ev") + private Integer ev; + + @ApiModelProperty("赠送多少优惠券") + private Long coupon; + + @ApiModelProperty("优惠券数量") + private Long couponCount; + + @ApiModelProperty("优惠券描述") + private String couponDesc; + + + public String getCouponDesc() { + if(couponCount.equals(1L)){ + return MessageFormat.format( I18nResources.CARD_NAME_CONTENT.getI18n(),"1"); + } + return MessageFormat.format( I18nResources.CARD_NAME_CONTENT.getI18n(),coupon.toString()) + " × " + couponCount; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/CheckoutResp.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/CheckoutResp.java new file mode 100644 index 0000000..8b39c9c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/CheckoutResp.java @@ -0,0 +1,38 @@ +package com.sonic.lion.domain.resp; + +import com.sonic.lion.enums.TradeStatus; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @author: code + * @date: 2025/05/20 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CheckoutResp { + + @ApiModelProperty(value = "提交ID") + private String submitId; + + @ApiModelProperty(value = "支付链接") + private String paymentUrl; + + @ApiModelProperty(value = "交易状态") + private TradeStatus tradeStatus; + + @ApiModelProperty(value = "手续费") + private Long fee; + + @ApiModelProperty(value = "流水ID列表【内部使用】", hidden = true) + private List bl; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/GetPayAccountFundThirdResp.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/GetPayAccountFundThirdResp.java new file mode 100644 index 0000000..230098f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/GetPayAccountFundThirdResp.java @@ -0,0 +1,30 @@ +package com.sonic.lion.domain.resp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GetPayAccountFundThirdResp { + /** + * 渠道ID + */ + private Integer channelId; + /** + * 第三方类型 + */ + private Integer appType; + /** + * 第三方关联ID + */ + private String openId; + /** + * 前端展示名称 + */ + private String displayName; + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/PrePaymentResp.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/PrePaymentResp.java new file mode 100644 index 0000000..c32e6dd --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/PrePaymentResp.java @@ -0,0 +1,35 @@ +package com.sonic.lion.domain.resp; + +import com.sonic.lion.enums.TradeStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/05/18 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PrePaymentResp { + + + /** + * 外部交易号(如订单号) + */ + private String outTradeNo; + + /** + * 外部交易号关联单号(如:打赏单号关联的游戏订单的订单号) + */ + private String outTradeNoRelationNo; + + private String tradeNo; + + private TradeStatus tradeStatus; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/QueryResp.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/QueryResp.java new file mode 100644 index 0000000..753cb86 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/QueryResp.java @@ -0,0 +1,38 @@ +package com.sonic.lion.domain.resp; + +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.TradeStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: code + * @date: 2025/05/20 + * @Description: + * @version: 1.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QueryResp { + + private String tradeNo; + + private TradeStatus tradeStatus; + + private String tradeMessage; + + + private BizType paymentBizType; + + private String paymentTradeNo; + + private String paymentTradeOrderNo; + + private Long chargeAmount; + + private Long payTime; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/SubscriptionDetailResp.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/SubscriptionDetailResp.java new file mode 100644 index 0000000..a04adf1 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/SubscriptionDetailResp.java @@ -0,0 +1,102 @@ +package com.sonic.lion.domain.resp; +/** + * { + * "status": "CANCELLED", + * "status_update_time": "2021-08-27T07:50:07Z", + * "id": "I-5E6A3FRHY0T6", + * "plan_id": "P-5CJ13061CV159492MMEUFKAA", + * "start_time": "2021-08-27T07:36:33Z", + * "quantity": "1", + * "shipping_amount": { + * "currency_code": "USD", + * "value": "0.0" + * }, + * "subscriber": { + * "email_address": "hulongcoder@qq.com", + * "payer_id": "4P3LS3G4JLXZW", + * "name": { + * "given_name": "九龙", + * "surname": "胡" + * }, + * "shipping_address": { + * "address": { + * "address_line_1": "温江区江安路739号", + * "admin_area_2": "成都市", + * "admin_area_1": "四川", + * "postal_code": "610000", + * "country_code": "C2" + * } + * } + * }, + * "billing_info": { + * "outstanding_balance": { + * "currency_code": "USD", + * "value": "0.0" + * }, + * "cycle_executions": [ + * { + * "tenure_type": "REGULAR", + * "sequence": 1, + * "cycles_completed": 1, + * "cycles_remaining": 0, + * "current_pricing_scheme_version": 1, + * "total_cycles": 0 + * } + * ], + * "last_payment": { + * "amount": { + * "currency_code": "USD", + * "value": "0.02" + * }, + * "time": "2021-08-27T07:36:48Z" + * }, + * "failed_payments_count": 0 + * }, + * "create_time": "2021-08-27T07:36:47Z", + * "update_time": "2021-08-27T07:50:07Z", + * "plan_overridden": false, + * "links": [ + * { + * "href": "https://api.paypal.com/v1/billing/subscriptions/I-5E6A3FRHY0T6", + * "rel": "self", + * "method": "GET" + * } + * ] + * } + */ + +import com.paypal.http.annotations.Model; +import com.paypal.http.annotations.SerializedName; +import lombok.Data; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +@Model +@Data +public class SubscriptionDetailResp { + + @SerializedName("id") + private String id; + @SerializedName("status") + private String status; + + @SerializedName("status_update_time") + private String status_update_time; + + @SerializedName("plan_id") + private String plan_id; + + + @SerializedName("start_time") + private String start_time; + + public LocalDateTime getPurchaseDate() { + String timestring = status_update_time.substring(0, status_update_time.length() - 1); + ZonedDateTime utcTime = LocalDateTime.parse(timestring, DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss")).atZone(ZoneId.from(ZoneOffset.UTC)); + ZonedDateTime beijingTime = utcTime.withZoneSameInstant(ZoneOffset.ofHours(8)); + return beijingTime.toLocalDateTime(); + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/SubscriptionReviseResp.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/SubscriptionReviseResp.java new file mode 100644 index 0000000..2c5c2b6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/SubscriptionReviseResp.java @@ -0,0 +1,68 @@ +package com.sonic.lion.domain.resp; + +import com.paypal.http.annotations.Model; +import com.paypal.http.annotations.SerializedName; +import com.paypal.orders.LinkDescription; +import lombok.Data; + +import java.util.List; + +/** + * @author code + * { + * "plan_id": "P-52J16157DW1386843MKQEYYI", + * "plan_overridden": false, + * "links": [ + * { + * "href": "https://www.paypal.com/webapps/billing/subscriptions/update?ba_token=BA-7T960188BR531494Y", + * "rel": "approve", + * "method": "GET" + * }, + * { + * "href": "https://api-m.paypal.com/v1/billing/subscriptions/I-YHK4D6794VNG", + * "rel": "edit", + * "method": "PATCH" + * }, + * { + * "href": "https://api-m.paypal.com/v1/billing/subscriptions/I-YHK4D6794VNG", + * "rel": "self", + * "method": "GET" + * }, + * { + * "href": "https://api-m.paypal.com/v1/billing/subscriptions/I-YHK4D6794VNG/cancel", + * "rel": "cancel", + * "method": "POST" + * }, + * { + * "href": "https://api-m.paypal.com/v1/billing/subscriptions/I-YHK4D6794VNG/suspend", + * "rel": "suspend", + * "method": "POST" + * }, + * { + * "href": "https://api-m.paypal.com/v1/billing/subscriptions/I-YHK4D6794VNG/capture", + * "rel": "capture", + * "method": "POST" + * } + * ] + * } + */ +@Model +@Data +public class SubscriptionReviseResp { + + /** + * 计划ID + */ + @SerializedName("plan_id") + private String plan_id; + + /** + * 链接 + */ + @SerializedName( + value = "links", + listClass = LinkDescription.class + ) + private List links; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/SummaryBillListResp.java b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/SummaryBillListResp.java new file mode 100644 index 0000000..92963ce --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/domain/resp/SummaryBillListResp.java @@ -0,0 +1,19 @@ +package com.sonic.lion.domain.resp; + +import com.sonic.common.rpc.Page; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class SummaryBillListResp { + + @ApiModelProperty("总收入") + private Long incomeTotal; + + @ApiModelProperty("总支出") + private Long outcomeTotal; + + @ApiModelProperty("分页列表") + private Page pageList; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/BindDataType.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/BindDataType.java new file mode 100644 index 0000000..37b1e97 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/BindDataType.java @@ -0,0 +1,43 @@ +package com.sonic.lion.enums; + +/** + * @author: code + * @date: 2025/06/05 + * @Description: + * @version: 1.0.0 + */ +public enum BindDataType { + + BANK_ACCOUNT(1, "BANK_ACCOUNT"), + + BANK_CARD(2, "BANK_CARD"), + + PAYPAL(3, "PAYPAL"), + ; + + private final int value; + + private final String desc; + + BindDataType(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public BindDataType get(int value) { + for (BindDataType bindDataType : values()) { + if (bindDataType.getValue() == value) { + return bindDataType; + } + } + return null; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/BizResultCode.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/BizResultCode.java new file mode 100644 index 0000000..7ee0497 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/BizResultCode.java @@ -0,0 +1,73 @@ +package com.sonic.lion.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum BizResultCode implements ApiResultCode { + + MISS_PARAM_ERROR("0001", "Missing parameter"), + + STATUS_ERROR("", "状态错误"), + + EXISTS_ERROR("", "数据已存在"), + + ; + + + private final String errorCode; + private final String errorMsg; + + BizResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + throw new BizException(this.getErrorCode(), MessageUtils.get(this.name())); + } + } + + /** + * 校验方法 + * @param expect + * @param code + * @param message + */ + public static void check(boolean expect, String code, String message) { + if (expect) { + throw new BizException(code, message); + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/BizType.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/BizType.java new file mode 100644 index 0000000..5b31970 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/BizType.java @@ -0,0 +1,130 @@ +package com.sonic.lion.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; + +/** + * @author: code + * @date: 2025/05/07 + * @Description: 交易类型 + * @version: 1.0.0 + */ +@Getter +public enum BizType { + + /** + * 游戏 + */ + GAME(10, "Game", TradeType.SECURED_TRANSACTION), + /** + * 充值 + */ + CHARGE(100, "Buff Charge", TradeType.CHARGE), + /** + * 订阅类型 + */ + SUB(150, "sub", TradeType.SUB), + /** + * buff 提现 + */ + WITHDRAW(200, "Withdraw", TradeType.WITHDRAW), + /** + * 退款 + */ + REFUND(300, "Refund", TradeType.REFUND), + /** + * 冻结余额 + */ + FROZEN_BALANCE(400, "Buff Frozen", TradeType.NON_SECURED_TRANSACTION), + /** + * 解冻余额 + */ + UNFROZEN_BALANCE(500, "Buff Unfrozen", TradeType.NON_SECURED_TRANSACTION), + /** + * 创建图片 + */ + CREATE_AI_IMAGE(610, "Create AI Image", TradeType.C2B), + /** + * 文本模型 + */ + TEXT_MODEL(620, "Text Model", TradeType.C2B), + /** + * 聊天辅助 + */ + CHAT_ASSISTANT(630, "Chat Assistant", TradeType.C2B), + /** + * 发送语音 + */ + SEND_VOICE(640, "Send Voice", TradeType.C2B), + /** + * 语音电话 + */ + VOICE_CALL(650, "Voice Call", TradeType.C2B), + /** + * 解锁上锁图片(10%计入官方虚拟账户,90%归用户虚拟钱包) + */ + IMAGE_UNLOCK(700, "Image Unlock", TradeType.NON_SECURED_TRANSACTION), + /** + * 购买心动值 + */ + HEARTBEAT_PURCHASE(800, "Heartbeat Purchase", TradeType.C2B), + /** + * 虚拟礼物(10%计入官方虚拟账户,90%归用户虚拟钱包) + */ + GIFT(900, "Gift", TradeType.NON_SECURED_TRANSACTION), + /** + * 解锁爱慕者 + */ + UNLOCK_ADMIRERS(1000, "Unlock Admirers", TradeType.C2B), + + /** + * 新注册用户赠送 + */ + NEW_USER_GIFT(1001, "New User Gift", TradeType.NON_SECURED_TRANSACTION), + + /** + * VIP用户赠送 + */ + VIP_BUFF_GIFT(1002, "VIP User Gift", TradeType.NON_SECURED_TRANSACTION), + + /** + * 用户签到赠送 + */ + SIGN_IN_GIFT(1003, "Sign In Gift", TradeType.NON_SECURED_TRANSACTION), + + + /** + * 订阅会员 + */ + SUBSCRIBE_MEMBER(1100, "Subscribe member", TradeType.C2B), + + ; + + private final int value; + + private final String desc; + + private final TradeType tradeType; + + + public String getI18nDescWhenSimple() { + return MessageUtils.get(this.name()); + } + + + BizType(int value, String desc, TradeType tradeType) { + this.value = value; + this.desc = desc; + this.tradeType = tradeType; + } + + public static BizType get(int value) { + for (BizType bizType : values()) { + if (bizType.getValue() == value) { + return bizType; + } + } + return null; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/FreeWithdrawReason.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/FreeWithdrawReason.java new file mode 100644 index 0000000..9dafdc7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/FreeWithdrawReason.java @@ -0,0 +1,34 @@ +package com.sonic.lion.enums; + +import lombok.Getter; + +@Getter +public enum FreeWithdrawReason { + + + /** + * 商家积分 + */ + SHOP_LEVEL("SHOP_LEVEL", "Congratulations, your store rating is 95 or above! We will waive the 10% commission when you pay while withdrawing." ), + + /** + * 天使用户 + */ + ANGEL_USER("ANGEL_USER", "Because you are the top 500 E-Pals, we will waive your 10% commission." ), + + + /** + * 运营 + */ + OPERATION("OPERATION", "Congratulations, we have waived your 10% commission!"), + + ; + + private String name; + private String desc; + + FreeWithdrawReason(String name, String desc) { + this.desc = desc; + this.name = name; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/I18nResources.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/I18nResources.java new file mode 100644 index 0000000..3194219 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/I18nResources.java @@ -0,0 +1,117 @@ +package com.sonic.lion.enums; + + +import com.sonic.common.utils.MessageUtils; + +public enum I18nResources { + + SHOP_LEVEL, + /** + * 天使用户 + */ + ANGEL_USER, + /** + * 运营 + */ + OPERATION, + + + /** + * 收入 + */ + INCOME, + + /** + * 支出 + */ + OUTCOME, + + + /** + * Gift Reward Spending + */ + BILL_GIFT_REWARD_SPENDING, + + /** + * Other Income + */ + BILL_OTHER_INCOME, + + /** + * Other Outcome + */ + BILL_OTHER_OUTCOME, + + /** + * Withdraw Fail + */ + BILL_WITHDRAW_FAIL, + + /** + * Buff Withdraw + */ + BILL_BUFF_WITHDRAW, + + /** + * Order Reward Refund + */ + BILL_ORDER_REWARD_REFUND, + + /** + * Game Refund + */ + BILL_GAME_REFUND, + + CARD_NAME_CONTENT, + + /** + * 下面这一批都是Income outcome + **/ + LIVE_REWARD_INCOME, + LIVE_REWARD_OUTCOME, + GAME_INCOME, + GAME_OUTCOME, + ORDER_REWARD_INCOME, + ORDER_REWARD_OUTCOME, + TIPS_REWARD_INCOME, + TIPS_REWARD_OUTCOME, + TOPIC_REWARD_INCOME, + TOPIC_REWARD_OUTCOME, + IM_REWARD_INCOME, + IM_REWARD_OUTCOME, + + //审核中 + IN_REVIEW, + //审核失败 + REVIEW_FAIL, + //提现中 + WITHDRAW_ING, + //提现失败 + WITHDRAW_FAIL, + WITHDRAW_SUCCESS, + GIFT_SPENDING, + GIFT_INCOME, + AWAITING_INCOME_STATUS, + BILL_MERCHANT_SUBSCRIBE_INCOME, + BILL_WITHDRAW_FAIL_BACK, + //6.19.1 版本新增【退还用户10%的提现手续费】 + REFUND_WITHDRAW_FEE_10, + //AI订阅收入 + BILL_AI_SUBSCRIBE_INCOME, + //AI礼物收入 + AI_GIFT_INCOME, + //AI礼物支出 + AI_GIFT_SPENDING, + //心愿打赏收入 + WISH_LIST_REWARD_INCOME, + //心愿打赏支出 + WISH_LIST_REWARD_OUTCOME, + //赠送VIP + GIFT_VIP, + ; + + public String getI18n() { + return MessageUtils.get(this.name()); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/MemberPrivEnum.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/MemberPrivEnum.java new file mode 100644 index 0000000..3dbdcad --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/MemberPrivEnum.java @@ -0,0 +1,19 @@ +package com.sonic.lion.enums; + +import lombok.Getter; + +@Getter +public enum MemberPrivEnum { + //增加coin + ADD_CRUSH_COIN, + //增加创建ai个数 + ADD_CREATE_AI, + //增加相册创建数 + ADD_ALBUM_CREATE, + //增加自动播放 + AUTO_PLAY_VOICE, + //增加自定义气泡 + CUSTOM_CHAT_BUBBLE, + //增加特殊礼物 + SPECIAL_GIFT, +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/MemberType.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/MemberType.java new file mode 100644 index 0000000..0d1ac8d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/MemberType.java @@ -0,0 +1,8 @@ +package com.sonic.lion.enums; + +public enum MemberType { + + VIP + ; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/PayChannel.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/PayChannel.java new file mode 100644 index 0000000..dc1cbb6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/PayChannel.java @@ -0,0 +1,48 @@ +package com.sonic.lion.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; + +/** + * @author: code + * @date: 2025/05/07 + * @Description: 支付渠道 + * @version: 1.0.0 + */ +public enum PayChannel { + + BUFF(0, "Buff"), + + STRIPE(1, "Stripe"), + + IAP(2, "Apple"), + + GOOGLE(3, "Google"), + ; + + private final int value; + + private final String desc; + + PayChannel(int value, String desc) { + this.value = value; + this.desc = desc; + } + + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static PayChannel get(int value) { + for (PayChannel payChannel : values()) { + if (payChannel.getValue() == value) { + return payChannel; + } + } + return null; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/PayGenrtatorCodeType.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/PayGenrtatorCodeType.java new file mode 100644 index 0000000..f323fe6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/PayGenrtatorCodeType.java @@ -0,0 +1,52 @@ +package com.sonic.lion.enums; + +/** + * 交易业务分类:根据业务分类 + * @author Xi.He + */ +public enum PayGenrtatorCodeType { + TRADE(1, "交易号"), + WITHDRAW(2, "提现号"), + CHANNEL_BILL(3,"支付流水号"), + ACCOUNT_BILL(4,"用户流水号"), + REFUND_BILL(5,"退款流水号"), + + SUB(6, "订阅"), + ; + /** 编码 */ + private int value; + /** 名称 */ + private String desc; + + PayGenrtatorCodeType(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static PayGenrtatorCodeType get(int value) { + for (PayGenrtatorCodeType payGenrtatorCodeType : values()) { + if (payGenrtatorCodeType.getValue() == value) { + return payGenrtatorCodeType; + } + } + return null; + } + + /** + * 根据站点类型获取交易枚举类型 + * @param siteType + * @return + */ + public static PayGenrtatorCodeType getTradeType(String siteType) { + return TRADE; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/PaymentMethod.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/PaymentMethod.java new file mode 100644 index 0000000..8de3d80 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/PaymentMethod.java @@ -0,0 +1,42 @@ +package com.sonic.lion.enums; + +/** + * @author: code + * @date: 2025/05/18 + * @Description: 支付方式 + * @version: 1.0.0 + */ +public enum PaymentMethod { + + /** + * 网页支付 + */ + WEB(1, "网页支付"), + ; + + private final int value; + + private final String desc; + + PaymentMethod(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static PaymentMethod get(int value) { + for (PaymentMethod paymentMethod : values()) { + if (paymentMethod.getValue() == value) { + return paymentMethod; + } + } + return null; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/PaymentType.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/PaymentType.java new file mode 100644 index 0000000..b84da51 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/PaymentType.java @@ -0,0 +1,50 @@ +package com.sonic.lion.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; + +/** + * @author: code + * @date: 2025/06/23 + * @Description: 支付方式 + * @version: 1.0.0 + */ +public enum PaymentType { + + + /** + * 三方支付渠道支付 + */ + CHANNEL(1, "CHANNEL"), + /** + * Buff balance + */ + BALANCE(0, "E-Pal Buff"), + + ; + + private final int value; + + private final String desc; + + PaymentType(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static PaymentType get(int value) { + for (PaymentType paymentType : values()) { + if (paymentType.getValue() == value) { + return paymentType; + } + } + return null; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/Platform.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/Platform.java new file mode 100644 index 0000000..c241d2d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/Platform.java @@ -0,0 +1,33 @@ +package com.sonic.lion.enums; + +/** + * @author: code + * @date: 2025/06/22 + * @Description: + * @version: 1.0.0 + */ +public enum Platform { + + /** + * 交易系统自身 + */ + PAY(1, "PAY"), + ; + + private final int value; + + private final String desc; + + Platform(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/StoreGoodsCategoryEnum.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/StoreGoodsCategoryEnum.java new file mode 100644 index 0000000..0acca3e --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/StoreGoodsCategoryEnum.java @@ -0,0 +1,36 @@ +package com.sonic.lion.enums; + +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; + +/** + * @description: 商城商城大类枚举 + * @author: mzc + * @date: 2024-09-11 11:08 + **/ +@Getter +public enum StoreGoodsCategoryEnum { + //勋章 + MEDAL("STORE_MEDAL_BUY_ITEM_DESC"), + //皮肤 + SKIN("STORE_SKIN_BUY_ITEM_DESC"), + + ; + /** + * 流水对应的描述 + */ + private String itemDesc; + + StoreGoodsCategoryEnum(String itemDesc) { + this.itemDesc = itemDesc; + } + + public static String getItemDesc(String storeGoodsCategory) { + for (StoreGoodsCategoryEnum value : StoreGoodsCategoryEnum.values()) { + if (value.name().equals(storeGoodsCategory)) { + return MessageUtils.get(value.getItemDesc()); + } + } + return null; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/ThirdAccountType.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/ThirdAccountType.java new file mode 100644 index 0000000..21ce6a1 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/ThirdAccountType.java @@ -0,0 +1,49 @@ +package com.sonic.lion.enums; + +/** + * @author: code + * @date: 2025/06/01 + * @Description: 三方账户类型,对应t_pay_account_fund_third app_type字段 + * @version: 1.0.0 + */ +public enum ThirdAccountType { + + /** + * 绑定类型 + */ + GOOGLE(1, "GOOGLE"), + APPLE(2, "APPLE"), + /** + * stripe 充值 + */ + STRIPE_PAYMENT(3, "STRIPE_PAYMENT"), + /** + * stripe 提现 + */ + STRIPE_PAYOUT(4, "STRIPE_PAYOUT"), + + ; + + private final int value; + private final String desc; + + ThirdAccountType(int value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static ThirdAccountType getWithdrawAccount(PayChannel payChannel) { + if (payChannel == PayChannel.STRIPE) { + return STRIPE_PAYOUT; + } + return null; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/ToastResultCode.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/ToastResultCode.java new file mode 100644 index 0000000..f16f00f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/ToastResultCode.java @@ -0,0 +1,184 @@ +package com.sonic.lion.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; + +import java.text.MessageFormat; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum ToastResultCode implements ApiResultCode { + + SYSTEM_EXCEPTION("0000", "系统异常"), + IP_EXCEPTION("0000", "系统异常"), + + PARAM_ERROR("0001", "参数异常"), + + PARAM_BLANK("0001", "参数不能为空"), + + /** + * No third-party account opened + */ + NO_THIRD_PARTY_ACCOUNT("", "三方账号不存在"), + + /** + * Unbound account data + */ + THIRD_PARTY_ACCOUNT_UNBIND("", "三方账户未绑定"), + + DATA_NOT_EXITS("", "数据不存在"), + + OUT_TRADE_NO_EMPTY("", "out trade no empty"), + + DATA_STATUS_INCORRECT("", "数据状态错误"), + + DATA_STATUS_CHANGED("1010", "数据状态已经变更请刷新后再试"), + /** + * Subscription platform must be paypal + */ + PLATFORM_PAYPAL("", "来源必须是paypal"), + /** + * The order has been changed + */ + PAY_TRADE_STATUS_CHANGE("", "交易状态已变更"), + + AMOUNT_LESS_THAN0("", "实付金额不能小于0"), + + AMOUNT_LESS_THAN_N("", "实付金额不能小于N"), + + PAY_TRADE_PAYER_PAYEE_ERROR("", "交易双方不能相同"), + + PAY_TRADE_PAYER_ERROR("", "this trade not for you"), + + PAY_TRADE_PAYMENT_TYPE_ERROR("", "payment type error"), + + /** + * The subscription must be active + */ + SUBSCRIPTION_STATUS_ERROR("", "paypal订阅状态错误"), + /** + * bind subscription error user id can't be null + */ + USER_ID_NOT_NULL("", "绑定用户id不能为空"), + /** + * 游戏订单支付失败 + * game order pay fail + */ + GAME_PAY_FAIL("1008", "game order pay fail"), + + INSUFFICIENT_BALANCE("INSUFFICIENT_BALANCE", "Insufficient balance"), + + INSUFFICIENT_BALANCE_WHEN_ORDER_FAIL("1008","Insufficient balance"), + + GOOGLE_TICKET_STATUS_ERROR("", "Incorrect ticket status"), + + REVEIVE_FAIL("", "没有达到领取条件"), + + REWARD_REVEIVED("", "已经被领取"), + + IAP_RECEIPT_STATUS_ERROR("", "收据不是成功状态"), + + IAP_BUNDLE_ID_ERROR("", "收据应用ID对应不上"), + + DUPLICATE_ORDER_NUMBER("", "Duplicate order number"), + + /** + * UNSUPPORTED_BUSINESS_TYPE + */ + UNSUPPORTED_BUSINESS_TYPE("", "不支持的业务类型"), + + IN_PROCESSING("1005", "tradeHandle processing. Please refresh and try again later"), + + RESOURCEKEY_IS_BLANK("", "resourceKey is blank"), + + RESOURCENUM_EXCEEDS_THE_MAXIMUM("", "resourceNum exceeds the maximum"), + + TRADE_NO_BLANK("", "tradeNo can not be"), + + TURN_ON_OFF("", "开关未开启"), + + KEY_ERROR("", "秘钥错误"), + + AUTH_ERROR("", ""), + + LIMIT_ERROR("", ""), + + CHARGE_CANCEL("2001", ""), + CHARGE_EXPIRED("2002", ""), + CHARGE_FAIL("2003", ""), + + SYS_PERMISSION_DENIED("权限不足", ""), + + CHANNEL_BLACK_ERROR("", "The current payment method is not supported for payment. Please use another payment method"), + + AIRWALLEX_CREATE_USER_ERROR("10044001", ""), + + CHANNEL_NOT_OPEN("", "Channel not open"), + + SUB_PRODUCT_NOT_FOUND("", "订阅商品不存在"), + + SUBSCRIBED_NO_DUPLICATE("", "订阅中,不允许重复订阅"), + + ; + + + private final String errorCode; + private final String errorMsg; + + ToastResultCode(String errorMsg) { + this.errorCode = ""; + this.errorMsg = errorMsg; + } + + ToastResultCode(String errorCode, String errorMsg) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg ;//+ "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1004"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + throw new BizException(this.getErrorCode(), MessageUtils.get(this.name())); + } + } + + public void check(boolean expect,String format) { + if (expect) { + throw new BizException(this.getErrorCode(), MessageFormat.format( MessageUtils.get(this.name()),format)); + } + } + + public void check(boolean expect, String errorCode, String message) { + if (expect) { + throw new BizException(errorCode, message); + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/TradeStatus.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/TradeStatus.java new file mode 100644 index 0000000..42574c6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/TradeStatus.java @@ -0,0 +1,74 @@ +package com.sonic.lion.enums; + +import org.springframework.lang.NonNull; + + +/** + * @author: code + * @date: 2025/05/07 + * @Description: 支付状态 + * @version: 1.0.0 + */ +public enum TradeStatus { + + WAITPAY(1, "待付款"), + + PAID(2, "已付款"), + + PROCESSING(3, "处理中"), + + FINISHED(4, "交易成功"), + + CLOSED(5, "交易关闭"), + + REFUNDING(6, "退款中"), + + REFUNDED(7, "已退款"), + ; + + private final int value; + + private final String desc; + + TradeStatus(Integer value, String desc) { + this.value = value; + this.desc = desc; + } + + public int getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static TradeStatus get(int value) { + for (TradeStatus tradeStatus : values()) { + if (tradeStatus.getValue() == value) { + return tradeStatus; + } + } + return null; + } + + /** + * 判断该交易状态是否是完成状态 + * + * @return + */ + public static boolean isComplete(@NonNull TradeStatus status) { + return status == FINISHED || status == CLOSED || status == REFUNDING || status == REFUNDED; + } + + public String getReponseString() { + if (this.value == 1 || this.value == 3 || this.value == 6) { + return "processing"; + } else if (this.value == 2 || this.value == 4 || this.value == 7) { + return "success"; + } else if (this.value == 5) { + return "failed"; + } + return "processing"; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/enums/TradeType.java b/sonic-lion/server/src/main/java/com/sonic/lion/enums/TradeType.java new file mode 100644 index 0000000..9d12ca8 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/enums/TradeType.java @@ -0,0 +1,61 @@ +package com.sonic.lion.enums; + +import lombok.Getter; + +/** + * @author: code + * @date: 2025/07/22 + * @Description: 交易类型 + * @version: 1.0.0 + */ +@Getter +public enum TradeType { + + /** + * 担保交易 + * 付款方先将钱付给系统担保账户,付款方确认完成服务后再将钱由系统担保账户转至收款方账户 + */ + SECURED_TRANSACTION(1, "Secured Transaction"), + + /** + * 非担保交易 + * 直接将钱从付款方账户转至收款方账户 + */ + NON_SECURED_TRANSACTION(2, "Non Secured Transaction"), + + /** + * 退款 + */ + REFUND(4, "Refund"), + + /** + * 提现 + */ + WITHDRAW(5, "Withdraw"), + + /** + * 充值 + */ + CHARGE(6, "Charge"), + + /** + * 用户付款给系统 + */ + C2B(7, "C2B"), + + /** + * 订阅 + */ + SUB(8, "Sub"), + + ; + + private final int value; + + private final String desc; + + TradeType(int value, String desc) { + this.value = value; + this.desc = desc; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/event/inner/EventType.java b/sonic-lion/server/src/main/java/com/sonic/lion/event/inner/EventType.java new file mode 100644 index 0000000..64bdcb0 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/event/inner/EventType.java @@ -0,0 +1,36 @@ +package com.sonic.lion.event.inner; + +import com.sonic.common.event.Event; +import com.sonic.lion.config.EventConfig; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义当前系统的消息 + * + * @author code + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** + * 事件定义 + */ + ACCOUNT_BUFF_BILL_SYNC_OPEN_SEARCH(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "account_buff_bill_sync_open_search", "流水同步到open search"), + + CHARGE_BUFF(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "charge_buff", "充值"), + + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/event/inner/payload/AccountBuffBillSyncOpenSearchPayload.java b/sonic-lion/server/src/main/java/com/sonic/lion/event/inner/payload/AccountBuffBillSyncOpenSearchPayload.java new file mode 100644 index 0000000..c3e6bb8 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/event/inner/payload/AccountBuffBillSyncOpenSearchPayload.java @@ -0,0 +1,43 @@ +package com.sonic.lion.event.inner.payload; + +import com.sonic.lion.enums.BizType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * 聊天室每天数据统计 + * + * @author mzc + * @date 2021-04-27 10:28 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AccountBuffBillSyncOpenSearchPayload { + + /** + * 流水idd + */ + private Long billId; + + /** + * 业务类型 + */ + private BizType bizType; + + /** + * 用户id + */ + private Long uid; + + + /** + * 目标用户id + */ + private Long desUid; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/event/inner/payload/ChargeBuffPayload.java b/sonic-lion/server/src/main/java/com/sonic/lion/event/inner/payload/ChargeBuffPayload.java new file mode 100644 index 0000000..d44db4c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/event/inner/payload/ChargeBuffPayload.java @@ -0,0 +1,31 @@ +package com.sonic.lion.event.inner.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * 聊天室每天数据统计 + * + * @author mzc + * @date 2021-04-27 10:28 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChargeBuffPayload { + + /** + * 用户ID + */ + private Long userId; + + /** + * 充值金额 + */ + private Long amount; + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/event/outer/EventType.java b/sonic-lion/server/src/main/java/com/sonic/lion/event/outer/EventType.java new file mode 100644 index 0000000..568850c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/event/outer/EventType.java @@ -0,0 +1,32 @@ +package com.sonic.lion.event.outer; + +import com.sonic.common.event.Event; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义不属于当前系统的消息 + * + * @author code + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** + * 事件定义 + */ + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + System.out.println(this.eventCode); + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/job/AccountBuffJob.java b/sonic-lion/server/src/main/java/com/sonic/lion/job/AccountBuffJob.java new file mode 100644 index 0000000..6e78243 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/job/AccountBuffJob.java @@ -0,0 +1,87 @@ +package com.sonic.lion.job; + +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.lion.domain.entity.AccountBuffAwaiting; +import com.sonic.lion.service.AccountBuffAwaitingService; +import com.sonic.lion.service.AccountBuffService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author: code + * @date: 2025/07/20 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@Component +public class AccountBuffJob { + + @Autowired + private AccountBuffService accountBuffService; + + @Autowired + private AccountBuffAwaitingService accountBuffAwaitingService; + + @Autowired + private JobmanClient jobmanClient; + + /** + * 每1分钟执行一次 + * 待入帐收入-->可提现收入 + */ + @Scheduled(cron = "0 0/1 * * * ?") + public void awaitingIncomeToWithdrawAbleIncomeSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("awaitingIncomeToWithdrawAbleIncomeJob", TimeUnit.MINUTES.toSeconds(2), this::awaitingIncomeToWithdrawAbleIncomeJob); + } + + protected JobmanClient.JobResult awaitingIncomeToWithdrawAbleIncomeJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> awaitingIncomeToWithdrawAbleIncomeJob start !!!"); + List list = accountBuffAwaitingService.listWillToIncome(200); + for (AccountBuffAwaiting accountBuffAwaiting : list) { + log.info("可提现buff入账, {}", accountBuffAwaiting); + try { + accountBuffService.awaitingIncomeToWithdrawableIncome(accountBuffAwaiting); + } catch (Exception e) { + log.error("可提现buff入账, id: " + accountBuffAwaiting.getId(), e); + } + } + log.info("===> awaitingIncomeToWithdrawAbleIncomeJob count : {} end !!!", 0); + return JobmanClient.JobResult.success(list.size()); + } finally { + LogUtils.removeTraceId(); + } + } + + + /** + * 每1分钟执行一次 + * 三天之前待入帐的可提现时间改成当前时间 处理 awaitingIncomeToWithdrawAbleIncomeJob 扫描漏数据的问题 + */ + @Scheduled(cron = "0 0/1 * * * ?") + public void awaitingIncomeUpdateOldDataSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("awaitingIncomeUpdateOldDataJob", TimeUnit.MINUTES.toSeconds(2), this::awaitingIncomeUpdateOldDataJob); + } + + protected JobmanClient.JobResult awaitingIncomeUpdateOldDataJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> awaitingIncomeUpdateOldDataJob start !!!"); + int count = accountBuffAwaitingService.updateOldData(); + log.info("===> awaitingIncomeUpdateOldDataJob count : {} end !!!", 0); + return JobmanClient.JobResult.success(count); + } finally { + LogUtils.removeTraceId(); + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/job/ProcessingJob.java b/sonic-lion/server/src/main/java/com/sonic/lion/job/ProcessingJob.java new file mode 100644 index 0000000..50c5faf --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/job/ProcessingJob.java @@ -0,0 +1,132 @@ +package com.sonic.lion.job; + +import com.sonic.common.AppRuntime; +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.common.utils.RedisLock; +import com.sonic.lion.service.ProcessingChargeService; +import com.sonic.lion.service.ProcessingWithdrawReviewService; +import com.sonic.lion.service.ProcessingWithdrawService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class ProcessingJob { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private ProcessingChargeService processingChargeService; + @Autowired + private ProcessingWithdrawService processingWithdrawService; + @Autowired + private ProcessingWithdrawReviewService processingWithdrawReviewService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private AppRuntime appRuntime; + + /** + * 每1分钟跑一次 + * 扫描正在充值中的数据,获取最终的充值状态 + */ + @Scheduled(cron = "10 0/1 * * * ?") + public void startChargeProcessingJobSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("chargeProcessingJob", TimeUnit.SECONDS.toSeconds(5 * 60), this::chargeProcessingJob); + } + + /** + * 每1分钟跑一次 + * 扫描过了1天审核时间,可以在三方发起提现转账操作的数据进行提现转账 + */ + @Scheduled(cron = "20 0/1 * * * ?") + public void startWithdrawReviewProcessingJobSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("withdrawReviewProcessingJob", TimeUnit.SECONDS.toSeconds(5 * 60), this::withdrawReviewProcessingJob); + } + + /** + * 每1分钟跑一次 + * 扫描提现中的数据获取最终的提现结果 + */ + @Scheduled(cron = "30 0/1 * * * ?") + public void startWithdrawProcessingJobSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("withdrawProcessingJob", TimeUnit.SECONDS.toSeconds(5 * 60), this::withdrawProcessingJob); + } + + /** + * 扫描正在充值中的数据,获取最终的充值状态 + */ + public JobmanClient.JobResult chargeProcessingJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> chargeProcessingJob start"); + long startTime = System.currentTimeMillis(); + //加锁,避免定时任务并发重复处理 + RedisLock redisLock = new RedisLock(appRuntime.buildPrefixKey("lock", "chargeProcessingJob"), redisWrapper); + redisLock.tryAcquireRun(10 * 60 * 1000, () -> { + processingChargeService.hand(); + return true; + }); + log.info("===> chargeProcessingJob end {} ms", System.currentTimeMillis() - startTime); + } catch (Exception e) { + log.error("===> chargeProcessingJob error : ", e); + } finally { + LogUtils.removeTraceId(); + } + return JobmanClient.JobResult.success(0); + } + + /** + * 扫描过了1天审核时间,可以在三方发起提现转账操作的数据进行提现转账 + * @param jobContext + * @return + */ + public JobmanClient.JobResult withdrawReviewProcessingJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> withdrawReviewProcessingJob start"); + long startTime = System.currentTimeMillis(); + //加锁,避免定时任务并发重复处理 + RedisLock redisLock = new RedisLock(appRuntime.buildPrefixKey("lock", "withdrawReviewProcessingJob"), redisWrapper); + redisLock.tryAcquireRun(10 * 60 * 1000, () -> { + processingWithdrawReviewService.hand(); + return true; + }); + log.info("===> withdrawReviewProcessingJob end {} ms", System.currentTimeMillis() - startTime); + } catch (Exception e) { + log.error("===> withdrawReviewProcessingJob error : ", e); + } finally { + LogUtils.removeTraceId(); + } + return JobmanClient.JobResult.success(0); + } + + /** + * 扫描提现中的数据获取最终的提现结果 + * @param jobContext + * @return + */ + public JobmanClient.JobResult withdrawProcessingJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> withdrawProcessingJob start"); + long startTime = System.currentTimeMillis(); + processingWithdrawService.hand(); + log.info("===> withdrawProcessingJob end {} ms", System.currentTimeMillis() - startTime); + } catch (Exception e) { + log.error("===> withdrawProcessingJob error : ", e); + } finally { + LogUtils.removeTraceId(); + } + return JobmanClient.JobResult.success(0); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/job/UserMemberGiftBuffJob.java b/sonic-lion/server/src/main/java/com/sonic/lion/job/UserMemberGiftBuffJob.java new file mode 100644 index 0000000..dd48d22 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/job/UserMemberGiftBuffJob.java @@ -0,0 +1,46 @@ +package com.sonic.lion.job; + + +import com.sonic.common.client.JobmanClient; +import com.sonic.common.utils.LogUtils; +import com.sonic.lion.service.MemberService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 用户会员每天赠送5个CrushCoin + */ +@Slf4j +@Component +public class UserMemberGiftBuffJob { + + @Autowired + private JobmanClient jobmanClient; + @Autowired + private MemberService memberService; + + + /** 每天1点执行一次 */ + @Scheduled(cron = "0 0 1 * * ?") + public void userMemberGiftBuffSchedule() { + // 锁 1 分钟过期 + jobmanClient.run("userMemberGiftBuffJob", TimeUnit.MINUTES.toSeconds(2), this::userMemberGiftBuffJob); + } + + protected JobmanClient.JobResult userMemberGiftBuffJob(JobmanClient.JobContext jobContext) { + try { + LogUtils.setTraceId(); + log.info("===> userMemberGiftBuffJob start !!!"); + Integer count = memberService.userMemberGiftBuffJob(); + log.info("===> userMemberGiftBuffJob count : {} end !!!", 0); + return JobmanClient.JobResult.success(count); + } finally { + LogUtils.removeTraceId(); + } + } + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/AccountBuffAwaitingService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/AccountBuffAwaitingService.java new file mode 100644 index 0000000..8f84fa5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/AccountBuffAwaitingService.java @@ -0,0 +1,73 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.AccountBuffAwaiting; +import org.springframework.lang.NonNull; + +import java.util.List; + +/** + * @author: code + * @date: 2025/07/17 + * @Description: + * @version: 1.0.0 + */ +public interface AccountBuffAwaitingService { + + /** + * 保存数据 + * + * @param accountBuffAwaiting + */ + void save(@NonNull AccountBuffAwaiting accountBuffAwaiting); + + /** + * 查询将要入账的记录 + * + * @param num + * @return + */ + List listWillToIncome(Integer num); + + /** + * 根据tradeNo查询 + * + * @param tradeNo + * @return + */ + AccountBuffAwaiting getByTradeNo(String tradeNo); + + List getListByTradeNo(String tradeNo); + + /** + * 更新带入账记录状态 + * + * @param id + * @param status + */ + void updateStatus(@NonNull Long id, @NonNull AccountBuffAwaiting.Status status, AccountBuffAwaiting.Status expectStatus); + + /** + * 更新待入账数据的buff数 + * + * @param id + * @param awaitingBuff + * @param expectStatus + */ + void updateAwaitingBuff(@NonNull Long id, @NonNull Long awaitingBuff, AccountBuffAwaiting.Status expectStatus); + + void fronzenBuffAwaiting(List tradeNoList); + + void unFronzenBuffAwaiting(List tradeNoList); + + boolean deleteById(Long id); + + int updateOldData(); + + /** + * 获取商家订阅待入账的数目 + * + * @param tradeNo + * @return + */ + int countMerchantSubscribeAwaitingIncome(String tradeNo,Long accountId); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/AccountBuffBillService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/AccountBuffBillService.java new file mode 100644 index 0000000..3f74d47 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/AccountBuffBillService.java @@ -0,0 +1,78 @@ +package com.sonic.lion.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Page; +import com.sonic.lion.domain.entity.AccountBuff; +import com.sonic.lion.domain.entity.AccountBuffBill; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.req.BillListReq; +import com.sonic.lion.domain.resp.BillOutput; +import com.sonic.lion.domain.resp.SummaryBillListResp; + +/** + * @author: code + * @date: 2025/07/17 + * @Description: + * @version: 1.0.0 + */ +public interface AccountBuffBillService extends IService { + + + /** + * 流水列表及汇总 + * + * @param req + * @param session + * @return + */ + SummaryBillListResp billListSummary(BillListReq req, Session session); + + /** + * 获取流水的汇总数据 + * + * @param billListReq + * @return + */ + default SummaryBillListResp summary(BillListReq billListReq, Long userId, Page page) { + return summary(billListReq, userId, page, false); + } + + SummaryBillListResp summary(BillListReq billListReq, Long userId, Page page, boolean excludCharge); + + + /** + * 为提现预生成流水表 + * + * @param input + * @param accountBuff + * @return + */ + Long insertBillWhenWithdraw(PayTrade input, AccountBuff accountBuff); + + /** + * 当提现失败时,增加返还流水 + * @param id + * @param accountBuff + */ + void insertBillWhenWithdrawFail(Long id, AccountBuff accountBuff); + + /** + * 更新提现状态 + * + * @param billId + * @param after + * @param message + * @return + */ + boolean updateWithdrawStatus(Long billId, AccountBuffBill.WithdrawStatus after, String message); + + + boolean updateWithdrawBillAmount(Long billId, Long occurAmount); + + /** + * 如果为退款且全额退款时,更新商家入帐流水为已退款 + * @param tradeNo + */ + void updateBuffTypeToRefund(String tradeNo); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/AccountBuffService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/AccountBuffService.java new file mode 100644 index 0000000..1ce374f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/AccountBuffService.java @@ -0,0 +1,137 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.AccountBuff; +import com.sonic.lion.domain.entity.AccountBuffAwaiting; +import com.sonic.lion.domain.entity.AppleRefundRecord; +import com.sonic.lion.domain.enums.BuffType; +import com.sonic.lion.domain.enums.InOrOut; +import com.sonic.lion.domain.input.BuffChangeInput; +import com.sonic.lion.domain.input.BuffTransferInput; +import com.sonic.lion.domain.output.WalletOutput; +import org.springframework.lang.NonNull; + +import java.util.List; + +/** + * @author: code + * @date: 2025/07/16 + * @Description: + * @version: 1.0.0 + */ +public interface AccountBuffService { + + /** + * 根据uid查询buff账户 + * + * @param uid + * @return + */ + AccountBuff getByUid(Long uid); + + /** + * 增加待入账金额 + * + * @param input + */ + Long addAwaitingIncome(@NonNull BuffChangeInput input); + + /** + * 减少充值金额 + * @param uid + * @param buff + * @return + */ + boolean frozenBalance(Long uid, Long buff); + + + /** + * 冻结余额 并发送消息 + * + * @param userId + * @param getBuffAmount + */ + AppleRefundRecord.FronzenStatus fronzenBalanceAndSendMessage(Long userId, Long getBuffAmount); + + /** + * 冻结钱包余额 + * @param uid + * @param buff + * @return + */ + boolean unFrozenBalance(Long uid, Long buff); + + /** + * 待入账金额入账 + * + * @param accountBuffAwaiting + */ + void awaitingIncomeToWithdrawableIncome(@NonNull AccountBuffAwaiting accountBuffAwaiting); + + /** + * 转账 + * @param input + * @return + */ + List transfer(@NonNull BuffTransferInput input); + + /** + * 计算总收入 + * + * @param accountBuff + * @return + */ + Long calculateIncome(AccountBuff accountBuff); + + /** + * 当提现失败 回滚 增加可提现金额 + * + * @param input + */ + void addWithdrawableIncome(@NonNull BuffChangeInput input); + + /** + * 保存流水 + * @param input + * @param accountBuff + * @param inOrOut + * @param buffType + * @return + */ + Long saveBill(BuffChangeInput input, AccountBuff accountBuff, InOrOut inOrOut, BuffType buffType); + + /** + * 加上在途余额 + * + * @param id + * @param amount + * @return + */ + void addWithdrawOnGoing(Long id, Long amount); + + + /** + * 扣减 在途余额 + * + * @param id + * @param amount + * @return + */ + boolean decWithdrawOnGoing(Long id, Long amount); + + + /** + * 在途资金 回滚到 可提现余额 + * + * @param id + * @param amount + * @return + */ + boolean withdrawOnGoingRollback(Long id, Long amount); + + /** + * 获取用户的钱包余额 + * @param uid + * @return + */ + WalletOutput wallet(Long uid); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/ActivityService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/ActivityService.java new file mode 100644 index 0000000..3cfe797 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/ActivityService.java @@ -0,0 +1,45 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.BuffRewardRecord; +import com.sonic.lion.domain.entity.PayCharge; +import com.sonic.lion.domain.enums.ProductRewardType; + +import java.time.LocalDateTime; +import java.util.List; + +public interface ActivityService { + + /** + * 根据用户得到 优惠类型 + * + * @param userId + * @return + */ + ProductRewardType getRewardTypeByUserId(Long userId, LocalDateTime createTime, String productId,String version); + + + /** + * 根据用户得到 优惠类型 + * + * @param userId + * @return + */ + List getRewardTypeByUserIdV2(Long userId, LocalDateTime createTime, List productIdList, String version); + + /** + * 根据用户得到 优惠类型 + * @param userId + * @param createTime + * @param productId + * @param version + * @return + */ + PayCharge getRewardTypeByUserIdV2(Long userId, LocalDateTime createTime, String productId, String version); + + /** + * 保存记录。 + * + * @param buffRewardRecord + */ + void saveRecord(BuffRewardRecord buffRewardRecord); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/AirwallexPaymentService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/AirwallexPaymentService.java new file mode 100644 index 0000000..fc87fe0 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/AirwallexPaymentService.java @@ -0,0 +1,37 @@ +package com.sonic.lion.service; + + +import com.sonic.lion.domain.input.PaymentBillInfo; + +/** + * @author: code + * @date: 2025/05/08 + * @Description: + * @version: 1.0.0 + */ +public interface AirwallexPaymentService { + + /** + * Airwallex渠道 创建用户 + * @param userId + * @param input + * @return + */ + String createCustomer(Long userId, PaymentBillInfo input); + + /** + * Airwallex渠道 修改用户信息 + * @param userId + * @param customerId + * @param input + */ + void updateCustomer(Long userId, String customerId, PaymentBillInfo input); + + /** + * 根据用户ID查询基础账号ID + * @param userId + * @return + */ + String queryCustomerByUserId(Long userId); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/AirwallexPayoutService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/AirwallexPayoutService.java new file mode 100644 index 0000000..75d8c35 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/AirwallexPayoutService.java @@ -0,0 +1,101 @@ +package com.sonic.lion.service; + + +import com.sonic.lion.domain.input.CreateBeneficiaryInput; +import com.sonic.lion.domain.input.GetFormSchemaInput; +import com.sonic.lion.domain.output.CreateBeneficiaryOutput; + +/** + * @author: code + * @date: 2025/05/08 + * @Description: + * @version: 1.0.0 + */ +public interface AirwallexPayoutService { + + /** + * Airwallex渠道在使用 + * @param input + * @return + */ + String getFormSchema(GetFormSchemaInput input); + + /** + * Airwallex渠道在使用 + * @param inputJson + * @return + */ + String getFormSchema(String inputJson); + + /** + * Airwallex渠道在使用 + * @param inputJson + * @return + */ + String validateFormSchema(String inputJson); + + /** + * 创建新的收款人 + * @param input + * @return + */ + CreateBeneficiaryOutput createBeneficiary(CreateBeneficiaryInput input); + + /** + * 创建新的收款人 + * @param beneficiaryId + * @return + */ + String getBeneficiary(String beneficiaryId); + + /** + * 更新新的收款人 + * @param beneficiaryId + * @param input + */ + CreateBeneficiaryOutput updateBeneficiary(String beneficiaryId, CreateBeneficiaryInput input); + + /** + * 模拟状态 + * @param paymentId + * @param failureType + * @param nextStatus + * @return + */ + String mockStats(String paymentId, String failureType, String nextStatus); + + /** + * 成功的付款又变成失败的检查 + * @param tradeNo + * @param batchId + * @param failureReason + */ + void airwallexSuccesToFailCheck(String tradeNo, String batchId, String failureReason); + +// /** +// * Airwallex渠道在使用 +// * @param input +// * @return +// */ +// String getApiSchema(GetApiSchemaInput input); +// +// /** +// * 获取收款人详细信息 +// * @param beneficiaryId +// * @return +// */ +// String getBeneficiary(String beneficiaryId); +// +// /** +// * 获取受益人列表 +// * @return +// */ +// String beneficiaryList(); +// +// /** +// * 获取转账列表 +// * @return +// */ +// String paymentsList(); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/BuffTransferService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/BuffTransferService.java new file mode 100644 index 0000000..334c642 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/BuffTransferService.java @@ -0,0 +1,23 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.AccountBuff; +import com.sonic.lion.domain.input.BuffTransferExtend; +import com.sonic.lion.domain.input.BuffTransferTargetInput; + +import java.util.List; +import java.util.Map; + +/** + * @author code + * buff 转账 服务 + */ +public interface BuffTransferService { + + /** + * 获取 buff 转账账户 + * @param uid + * @return + */ + AccountBuff getAndInitAccount(Long uid); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/CashierService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/CashierService.java new file mode 100644 index 0000000..7c937bf --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/CashierService.java @@ -0,0 +1,24 @@ +package com.sonic.lion.service; + +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.req.CheckoutReq; +import com.sonic.lion.domain.req.TradeQueryReq; +import com.sonic.lion.domain.resp.CheckoutResp; +import com.sonic.lion.domain.resp.QueryResp; + +public interface CashierService { + + /** + * 结账 + * @param userId + * @param req + */ + CheckoutResp checkout(Long userId, CheckoutReq req); + + /** + * 结账结果查询 + * @param req + * @return + */ + Result query(TradeQueryReq req); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/ChannelBlacklistService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/ChannelBlacklistService.java new file mode 100644 index 0000000..506eab3 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/ChannelBlacklistService.java @@ -0,0 +1,18 @@ +package com.sonic.lion.service; + +/** + * @Author code + * @Description 渠道黑名单列表 + * @Date 2024/1/23 15:06 + * @Version 1.0 + */ +public interface ChannelBlacklistService { + + /** + * 添加数据到黑名单列表中 + * @param userId + * @param channelType + */ + void addBlacklist(Long userId, String channelType); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/ChannelProcessingService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/ChannelProcessingService.java new file mode 100644 index 0000000..7674765 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/ChannelProcessingService.java @@ -0,0 +1,16 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.PayCallChannelRecord; +import com.sonic.lion.domain.output.SyncChannelOutput; + +/** + * 渠道处理服务 + * 用于抽象 渠道的 交易发起 交易回调 交易超时 交易关闭 + */ +public interface ChannelProcessingService { + + SyncChannelOutput chargeProcessing(PayCallChannelRecord record); + + SyncChannelOutput withdrawProcessing(PayCallChannelRecord record); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/ChargeProductConfigService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/ChargeProductConfigService.java new file mode 100644 index 0000000..d011ccd --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/ChargeProductConfigService.java @@ -0,0 +1,23 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.output.ChargeProductConfigOutput; + +/** + * @Author code + * @Description 充值商品配置 + * @Date 2024/5/27 16:56 + * @Version 1.0 + */ +public interface ChargeProductConfigService { + + /** + * 获取充值档位的相关配置数据 + * @param currentUserId + * @param platform + * @param version + * @return + */ + ChargeProductConfigOutput getRechargeLevelConfig(Long currentUserId, String platform, String version); + + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/CheckOutService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/CheckOutService.java new file mode 100644 index 0000000..4817ec9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/CheckOutService.java @@ -0,0 +1,20 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.req.PrePaymentReq; +import com.sonic.lion.domain.resp.BuffCheckoutResp; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +public interface CheckOutService { + + @Transactional(propagation = Propagation.REQUIRES_NEW) + void chargeAfterPayment(String paymentTradeNo, Long srcUid); + + /** + * 预下单并结账 + * @param req + * @return + */ + BuffCheckoutResp checkout(PrePaymentReq req); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/CommonMessageService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/CommonMessageService.java new file mode 100644 index 0000000..1326409 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/CommonMessageService.java @@ -0,0 +1,24 @@ +package com.sonic.lion.service; + + +import java.time.LocalDateTime; + +/** + * 公共发送系统通知 + */ +public interface CommonMessageService { + + /** + * 会员续期成功通知 + * + * @param + */ + void memberRenewSuccess(Long userId, LocalDateTime expireTime); + + /** + * 会员续期失败通知 + * + * @param + */ + void memberRenewFail(Long userId); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/CommonSendMqService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/CommonSendMqService.java new file mode 100644 index 0000000..48ecb25 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/CommonSendMqService.java @@ -0,0 +1,11 @@ +package com.sonic.lion.service; + +/** + * @description: 发送消息到mq + * @author: code + * @create: 2025-02-06 17:56 + **/ +public interface CommonSendMqService { + +} + diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/FreeWithdrawConfigService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/FreeWithdrawConfigService.java new file mode 100644 index 0000000..b63ef68 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/FreeWithdrawConfigService.java @@ -0,0 +1,48 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.FreeWithdrawConfig; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.input.WithdrawFeeInput; +import com.sonic.lion.domain.output.WithdrawFeeReduceInfoOutput; +import com.sonic.lion.domain.req.FreeWithdrawReq; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 计算 提现费用 和 免费的费用都 写在这块 + */ +public interface FreeWithdrawConfigService { + + void setFreeWithdraw(FreeWithdrawReq freeWithdraw); + + /** + * 是否免费 如果免费 返回免费的配置 否则返回null + * + * @param uid + * @param withdrawRequestTime + * @return + */ + FreeWithdrawConfig isFree(Long uid, LocalDateTime withdrawRequestTime); + + List getAllFreeUserIds(); + + FreeWithdrawConfig getFreeReson(Long uid); + + + /** + * 计算提现的费用 + * + * @param payTrade + * @return + */ + WithdrawFeeInput calculateWithdrawFee(PayTrade payTrade); + + /** + * 获取用户的提现减免信息 + * + * @param userId + * @return + */ + WithdrawFeeReduceInfoOutput getWithdrawFeeReduceInfo(Long userId); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/GoogleRecordService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/GoogleRecordService.java new file mode 100644 index 0000000..7ed6cd8 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/GoogleRecordService.java @@ -0,0 +1,27 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.GoogleRecord; +import org.springframework.lang.NonNull; + +/** + * @author: code + * @date: 2025/06/24 + * @Description: + * @version: 1.0.0 + */ +public interface GoogleRecordService { + + /** + * 保存 + * + * @param googleRecord + */ + void save(@NonNull GoogleRecord googleRecord); + + /** + * 根据iap交易id查询 + * + * @param transactionId + */ + GoogleRecord getByTransactionId(String transactionId); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/GoogleService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/GoogleService.java new file mode 100644 index 0000000..1ec71a3 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/GoogleService.java @@ -0,0 +1,107 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.PayCharge; +import com.sonic.lion.domain.req.GoogleUploadReceiptReq; +import com.google.api.services.androidpublisher.model.ProductPurchase; +import com.google.api.services.androidpublisher.model.SubscriptionPurchase; +import com.google.api.services.androidpublisher.model.VoidedPurchase; +import lombok.Builder; +import lombok.Data; +import org.springframework.lang.NonNull; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author: code + * @date: 2025/06/24 + * @Description: + * @version: 1.0.0 + */ +public interface GoogleService { + + /** + * 计算付款手续费 + * @param productAmount + * @return + */ + Long calculatePaymentFee(@NonNull Long productAmount); + + /** + * 计算付款手续费 + * @param payCharge + * @return + */ + Long calculatePaymentFee(PayCharge payCharge); + + /** + * 上传收据 + * @param req + */ + void uploadReceipt(@NonNull GoogleUploadReceiptReq req) throws GeneralSecurityException, IOException; + + /** + * 补偿处理客户端已上传还未处理的收据 + */ + void processReceipt(); + + + /** + * google的token校验 + */ + ProductPurchase googleCheck(String productId, String token) throws Exception; + + SubscriptionPurchase getSubscriptionPurchase(String productId, String token) throws GeneralSecurityException, IOException; + + void handReceiptSubscribe(String productId, Long receiptId, Long userId); + + + List voidedPurchases(LocalDateTime startTime) throws GeneralSecurityException, IOException; + + boolean handSubscribeWebhook(Long notifyId, String type, String playload) throws Exception; + + + @Builder + @Data + class GoogleTokenResult { + + /** + * 开发者指定的字符串,其中包含有关订单的补充信息。 + */ + private String developerPayload; + + /** + * 订单的购买状态。可能的值为:0。已购买1.已取消2.待定 + */ + private int purchaseState; + + /** + * 产品购买的时间,自该时期(1970年1月1日)以毫秒为单位 + */ + private Long purchaseTimeMillis; + + /** + * 表示androidpublisher服务中的inappPurchase对象 + */ + private String kind; + + /** + * 与inapp产品购买相关的订单ID。 + */ + private String orderId; + + /** + * inapp产品的购买类型。仅当未使用标准应用内结算流程进行购买时,才设置此字段。 + * 可能的值为:0。测试(即从许可证测试帐户购买)1.促销(即使用促销代码购买)2.奖励(即观看视频广告而不是付费) + */ + private String purchaseType; + + /** + * Inapp产品的确认状态。可能的值为:0。尚未确认1.已确认 + */ + private String acknowledgementState; + + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/GoogleUploadReceiptService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/GoogleUploadReceiptService.java new file mode 100644 index 0000000..a4a9b27 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/GoogleUploadReceiptService.java @@ -0,0 +1,52 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.GoogleUploadReceipt; +import org.springframework.lang.NonNull; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author: code + * @date: 2025/07/15 + * @Description: + * @version: 1.0.0 + */ +public interface GoogleUploadReceiptService { + + /** + * 保存收据 + * + * @param googleUploadReceipt + */ + Long save(@NonNull GoogleUploadReceipt googleUploadReceipt); + + + GoogleUploadReceipt getById(Long id); + + /** + * 查询是否存在了这个凭据 + * + * @param receipt + * @return + */ + boolean countRecepit(String receipt); + + /** + * 更新是否处理 + * + * @param id + * @param processed + * @return + */ + int updateProcessed(@NonNull Long id, @NonNull Boolean processed); + + /** + * 查询未处理的收据 + * + * @param processed + * @param startCreateTime + * @return + */ + List list(Boolean processed, LocalDateTime startCreateTime, Integer num); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/IapRecordService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/IapRecordService.java new file mode 100644 index 0000000..639f7d0 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/IapRecordService.java @@ -0,0 +1,27 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.IapRecord; +import org.springframework.lang.NonNull; + +/** + * @author: code + * @date: 2025/06/24 + * @Description: + * @version: 1.0.0 + */ +public interface IapRecordService { + + /** + * 保存 + * + * @param iapRecord + */ + void save(@NonNull IapRecord iapRecord); + + /** + * 根据iap交易id查询 + * + * @param transactionId + */ + IapRecord getByTransactionId(String transactionId); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/IapService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/IapService.java new file mode 100644 index 0000000..5cb53b1 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/IapService.java @@ -0,0 +1,86 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.req.IapUploadReceiptReq; +import org.springframework.lang.NonNull; + +/** + * @author: code + * @date: 2025/06/24 + * @Description: + * @version: 1.0.0 + */ +public interface IapService { + + /** + * 计算付款手续费 + * + * @param productAmount + * @return + */ + Long calculatePaymentFee(@NonNull Long productAmount); + + /** + * 上传收据 + * @param req + * @param currentUserId + */ + void uploadReceipt(@NonNull IapUploadReceiptReq req, Long currentUserId); + + /** + * 补偿处理客户端已上传还未处理的收据 + */ + void processReceipt(); + + /** + * 处理第一次订阅 + * + * @param productId + * @param receiptId + * @param userId + * @return + */ + boolean handReceiptSubscribe(String productId, Long receiptId, Long userId); + + + /** + * 处理订阅的 回调通知 + * @param + * @return + */ + boolean handSubscribeWebhook(Long receiptId, String playload); + + /** + * 处理状态变更的回调 + * @param receiptId + * @param playload + */ + void doWhenStatusChange(Long receiptId, String playload); + + /** + * 处理 CANCEL 的通知类型 + * @param receiptId + * @param playlod + */ + void doWhenCancel(Long receiptId, String playlod); + + /** + * 处理付款续订的通知类型 + * @param receiptId + * @param playlod + */ + void doWhenPay(Long receiptId, String playlod); + + /** + * 处理付款续订失败的通知类型 + * @param receiptId + * @param playlod + */ + void doWhenPayFail(Long receiptId, String playlod); + + /** + * 处理发起争议的通知类型 + * @param receiptId + * @param playload + */ + void doWhenConsumptionRequest(Long receiptId, String playload); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/IapUploadReceiptService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/IapUploadReceiptService.java new file mode 100644 index 0000000..5140345 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/IapUploadReceiptService.java @@ -0,0 +1,43 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.IapUploadReceipt; +import org.springframework.lang.NonNull; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author: code + * @date: 2025/07/15 + * @Description: + * @version: 1.0.0 + */ +public interface IapUploadReceiptService { + + IapUploadReceipt getById(Long id); + + /** + * 保存收据 + * + * @param iapUploadReceipt + */ + Long save(@NonNull IapUploadReceipt iapUploadReceipt); + + /** + * 更新是否处理 + * + * @param id + * @param processed + * @return + */ + int updateProcessed(@NonNull Long id, @NonNull Boolean processed); + + /** + * 查询未处理的收据 + * + * @param processed + * @param startCreateTime + * @return + */ + List list(Boolean processed, LocalDateTime startCreateTime, Integer num); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/IapV2Service.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/IapV2Service.java new file mode 100644 index 0000000..a0e6677 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/IapV2Service.java @@ -0,0 +1,52 @@ +package com.sonic.lion.service; + +/** + * @author: code + * @date: 2025/06/24 + * @Description: + * @version: 1.0.0 + */ +public interface IapV2Service { + + /** + * 处理订阅的 回调通知 + * @param + * @return + */ + boolean handSubscribeWebhook(Long receiptId, String payload); + + /** + * 处理状态变更的回调 + * @param receiptId + * @param payload + */ + void doWhenStatusChange(Long receiptId, String payload); + + /** + * 处理 CANCEL 的通知类型 + * @param receiptId + * @param payload + */ + void doWhenCancel(Long receiptId, String payload); + + /** + * 处理付款续订的通知类型 + * @param receiptId + * @param payload + */ + void doWhenPay(Long receiptId, String payload); + + /** + * 处理付款续订失败的通知类型 + * @param receiptId + * @param payload + */ + void doWhenPayFail(Long receiptId, String payload); + + /** + * 处理发起争议的通知类型 + * @param receiptId + * @param payload + */ + void doWhenConsumptionRequest(Long receiptId, String payload); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/MemberPrivDictService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/MemberPrivDictService.java new file mode 100644 index 0000000..c2d1d5c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/MemberPrivDictService.java @@ -0,0 +1,19 @@ +package com.sonic.lion.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.lion.domain.MemberPrivDict; + +import java.util.List; + +/** + * 会员特权字典表 服务类 + */ +public interface MemberPrivDictService extends IService { + + /** + * 获取会员特权列表 + * + * @return + */ + List getMemberPrivList(); +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/MemberService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/MemberService.java new file mode 100644 index 0000000..54ef97d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/MemberService.java @@ -0,0 +1,19 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.output.MemberDetailOutput; + +public interface MemberService { + + /** + * 会员详情 + * + * @param userId + * @return + */ + MemberDetailOutput memberDetail(Long userId); + + /** + * 用户是会员,每天赠送5个Coin + */ + Integer userMemberGiftBuffJob(); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/PayAccountFundThirdService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayAccountFundThirdService.java new file mode 100644 index 0000000..cad61d8 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayAccountFundThirdService.java @@ -0,0 +1,58 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.PayAccountFundThird; +import com.sonic.lion.domain.input.AccountGenerateInput; +import com.sonic.lion.domain.input.BindBankCardInput; +import com.sonic.lion.enums.ThirdAccountType; +import org.springframework.lang.NonNull; + +/** + * @author: code + * @date: 2025/05/12 + * @Description: + * @version: 1.0.0 + */ +public interface PayAccountFundThirdService { + + /** + * 查询用户三方openId + * + * @param accountId + * @param thirdAccountType + * @return + */ + PayAccountFundThird getByAccountIdAndAppType(Long accountId, ThirdAccountType thirdAccountType); + + /** + * 根据三方账户openId和类型查询对应账户id + * + * @param openId + * @param thirdAccountType + * @return + */ + Long getAccountIdByOpenId(String openId, ThirdAccountType thirdAccountType); + + /** + * 用户三方账号 + * + * @param input + */ + void generate(@NonNull AccountGenerateInput input); + + /** + * 绑定银行卡 + * + * @param input + */ + void bindBankCard(@NonNull BindBankCardInput input); + + /** + * 绑定stripe的账号 + * @param uid + * @param customerId + * @return + * @throws Exception + */ + PayAccountFundThird bindStripe(Long uid, String customerId); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/PayCallChannelService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayCallChannelService.java new file mode 100644 index 0000000..84e92ba --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayCallChannelService.java @@ -0,0 +1,123 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.PayCallChannelRecord; +import com.sonic.lion.domain.enums.CallChannelStatus; +import com.sonic.lion.domain.input.ChannelBindBankCardInput; +import com.sonic.lion.domain.input.ChannelCreateCustomerInput; +import com.sonic.lion.domain.input.ChannelPaymentInput; +import com.sonic.lion.domain.input.ChannelPayoutInput; +import com.sonic.lion.domain.output.*; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import org.springframework.lang.NonNull; + +import java.util.List; + +/** + * @author: code + * @date: 2025/05/13 + * @Description: + * @version: 1.0.0 + */ +public interface PayCallChannelService { + + /** + * 根据主键查询 + * + * @param id + * @return + */ + PayCallChannelRecord get(Long id); + + /** + * 根据tradeNo查询该笔交易最后一次调用支付渠道的记录 + * + * @param tradeNo + * @param bizType + * @return + */ + PayCallChannelRecord getByTradeNoAndBizTypeLast(String tradeNo, BizType bizType); + + /** + * 检查未确定的调用 + */ + @Deprecated + List waitCheckRecord(); + + /** + * 调用支付渠道提现 + * + * @param input + */ + ChannelPayoutOutput callPayout(@NonNull ChannelPayoutInput input); + + /** + * web支付 + * + * @param input + */ + ChannelPaymentOutput callPayment(@NonNull ChannelPaymentInput input); + + /** + * @param id + * @param status + * @param expectStatus + * @return + */ + void updateStatus(@NonNull Long id, @NonNull CallChannelStatus status, @NonNull CallChannelStatus expectStatus); + + /** + * 计算手续费 + * + * @param amount + * @param payChannel + * @param bizType + * @return + */ + Long calculateFee(@NonNull Long amount, @NonNull PayChannel payChannel, @NonNull BizType bizType); + + /** + * 计算平台抽成 + * @param amount + * @param bizType + * @return + */ + Long calculatePlatformFee(@NonNull Long amount, @NonNull BizType bizType); + + /** + * 调用渠道创建customer + * + * @param input + */ + ChannelCreateCustomerOutput callCreateCustomer(@NonNull ChannelCreateCustomerInput input); + + /** + * 检查调用记录 + * + * @param record + * @return + */ + ChannelCheckOutput checkRecord(PayCallChannelRecord record); + + /** + * 调用支付渠道绑定银行卡 + * + * @param input + * @return + */ + ChannelBindBankCardOutput callBindBankCard(@NonNull ChannelBindBankCardInput input); + + /** + * 保存 调用结果 + * + * @param payCallChannelRecord + */ + void save(PayCallChannelRecord payCallChannelRecord); + + /** + * 保存渠道账单 + * + * @param record + */ + void saveChannelBill(PayCallChannelRecord record); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/PayChannelRouterService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayChannelRouterService.java new file mode 100644 index 0000000..7a90127 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayChannelRouterService.java @@ -0,0 +1,22 @@ +package com.sonic.lion.service; + +import com.sonic.lion.enums.PayChannel; +import org.springframework.lang.NonNull; + +/** + * @author: code + * @date: 2025/06/01 + * @Description: + * @version: 1.0.0 + */ +public interface PayChannelRouterService { + + /** + * 获取支付渠道service + * + * @param payChannel + * @return + */ + PayChannelService getPayChannelService(@NonNull PayChannel payChannel); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/PayChannelService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayChannelService.java new file mode 100644 index 0000000..6502d45 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayChannelService.java @@ -0,0 +1,173 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.bo.SubscriptionBo; +import com.sonic.lion.domain.bo.WebhookBo; +import com.sonic.lion.domain.input.*; +import com.sonic.lion.domain.output.*; +import org.springframework.lang.NonNull; + +import java.math.BigDecimal; + +/** + * @author: code + * @date: 2025/05/08 + * @Description: + * @version: 1.0.0 + */ +public interface PayChannelService { + + /** + * web支付 + * + * @param input + * @return + */ + ChannelPaymentOutput payment(@NonNull ChannelPaymentInput input); + + /** + * 付款 + * + * @param input + */ + ChannelPayoutOutput payout(@NonNull ChannelPayoutInput input); + + /** + * 计算提现手续费 + * + * @param occurAmount + * @return + */ + Long calculateWithdrawFee(@NonNull Long occurAmount); + + /** + * 检查付款请求 + * + * @param channelSn + * @return + */ + ChannelCheckOutput checkPayout(String channelSn); + + /** + * 检查收款请求 + * + * @param input + * @return + */ + ChannelCheckOutput checkPayment(@NonNull ChannelCheckPaymentInput input); + + /** + * 检查退款请求 + * + * @param input + * @return + */ + ChannelCheckOutput checkRefund(@NonNull ChannelCheckRefundInput input); + + /** + * 创建用户的订阅付款链接 + * @param priceId + * @param customerId + * @param successUrl + * @param cancelUrl + * @param trialPeriodDays + * @return + */ + ChannelPaymentOutput createSubPayment(String priceId, String customerId, String successUrl, String cancelUrl, Long trialPeriodDays); + + /** + * 查询用户的订阅付款链接 + * @param sessionId + * @return + */ + String retrieveSessionUrl(String sessionId); + + /** + * 查询用户的订阅付款列表 + * @param input + * @return + */ + SubscriptionBo checkSubPayment(ChannelSubPayoutInput input); + + /** + * 计算支付手续费 + * + * @param productAmount + * @return + */ + Long calculatePaymentFee(@NonNull Long productAmount); + + /** + * 为用户创建三方账户 + * + * @return + */ + ChannelCreateCustomerOutput createCustomer(@NonNull ChannelCreateCustomerInput input); + + /** + * 取消交易 + * + * @param transactionId + */ + ChannelCancelOutput cancel(@NonNull String transactionId); + + /** + * 全额退款 + * + * @param input + */ + ChannelRefundOutput refund(@NonNull ChannelRefundInput input); + + /** + * 支付渠道绑定银行卡 + * + * @param input + * @return + */ + ChannelBindBankCardOutput bindBankCard(@NonNull ChannelBindBankCardInput input); + + /** + * 绑定Paypal账号 + * + * @param input + * @return + */ + ChannelBindPaypalOutput bindPayPal(@NonNull ChannelBindPaypalInput input); + + /** + * 绑定银行账户 + * + * @param input + * @return + */ + ChannelBindBankAccountOutput bindBankAccount(@NonNull ChannelBindBankAccountInput input); + + /** + * 获取付款手续费基数 + * + * @return + */ + Long getPaymentFeeBase(); + + /** + * 获取付款手续费费率 + * + * @return + */ + BigDecimal getPaymentFeeRate(); + + /** + * webhook前置处理,解析、校验签名 + * @param input + * @param webhookSecret + * @return + */ + WebhookBo webhookBeforeHandler(@NonNull ChannelWebhookInput input, String webhookSecret); + + /** + * webhook后置处理 + * + * @param input + * @return + */ + ChannelWebhookOutput webhookAfterHandler(@NonNull ChannelWebhookInput input) throws Exception; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/PayChargeService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayChargeService.java new file mode 100644 index 0000000..84ac6c5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayChargeService.java @@ -0,0 +1,81 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.AppStoreProduct; +import com.sonic.lion.domain.entity.PayCharge; +import com.sonic.lion.domain.input.SubChargeProductListInput; +import com.sonic.lion.domain.output.SubProductListOutput; + +import java.util.List; +import java.util.Map; + +/** + * @author: code + * @date: 2025/06/24 + * @Description: + * @version: 1.0.0 + */ +public interface PayChargeService { + + /** + * 根据应用id和产品id查询 + * + * @param bundleId + * @param productId + * @return + */ + PayCharge getByBundleIdAndProductIdAndPlatform(String bundleId, String productId,String platform); + + /** + * 根据产品id查询 + * @param productId + * @param platform + * @return + */ + PayCharge getByBundleIdAndProductIdAndPlatform( String productId,String platform); + + /** + * 列表 + * + * @return + */ + List list(String platform); + + /** + * 列表 + * @param platform + * @param version + * @return + */ + List list(String platform,String version); + + /** + * 获取订阅档位列表 + * @param input + * @return + */ + List subProductList(SubChargeProductListInput input); + + /** + * 获取AppStore产品 + * @param productId + * @param platform + * @return + */ + AppStoreProduct getAppStoreProduct(String productId, String platform); + + /** + * 获取充值档位基础数据 + * @param productId + * @return + */ + PayCharge getByProductId(String productId); + + /** + * 获取充值档位基础数据 + * @param productId + * @param platform + * @return + */ + PayCharge getByProductId(String productId, String platform); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/PayConfigService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayConfigService.java new file mode 100644 index 0000000..17bd965 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayConfigService.java @@ -0,0 +1,115 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.output.EnabledPayChannelOutput; +import com.sonic.lion.enums.PayChannel; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 支付配置 + */ +public interface PayConfigService { + + /** + * 通过key查询配置value + * + * @param key + * @return + */ + String getByKey(String key); + + /** + * Google 支付费用比例 例如:1/3 + * + * @return + */ + String getGoogleFeeRatio(); + + /** + * 安卓 支付费用比例 例如:2/3 + * + * @return + */ + String getIOSFeeRatio(); + + /** + * 获取所有开启的支付渠道 + * + * @return + */ + List getEnabledPayChannel(); + + /** + * 获取所有支付渠道 + * + * @return + */ + List getAllPayChannelDebug(); + + /** + * 获取所有开启的提现渠道 + * + * @return + */ + List getEnabledPayOutChannel(); + + /** + * 充值渠道开关状态校验 + * @param currentUserId + * @param payChannel + * @return + */ + boolean paymentChannelSwitchCheckPass(Long currentUserId, PayChannel payChannel); + + /** + * 提现渠道开关状态校验 + * @param currentUserId + * @param payChannel + * @return + */ + boolean payoutChannelSwitchCheckPass(Long currentUserId, PayChannel payChannel); + + /** + * 获取测试人员配置 + * @return + */ + List getDebugPayUserIds(); + + /** + * 查询是否允许 首单 奖励 + * + * @return + */ + boolean enableFirstOrderReward(); + + + /** + * 查询是否允许 周五限时奖励 + * + * @return + */ + boolean enableEveryFridayReward(); + + /** + * 是否开启提现手续费减免 + * + * @return + */ + boolean enableWithdrawFeeReduction(); + + /** + * 提现手续费减免结束时间 + * + * @return + */ + LocalDateTime withdrawFeeReductionEndTime(); + + /** + * 可用的支付渠道 + * @param userId + * @return + */ + EnabledPayChannelOutput enabledPayChannel(Long userId); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/PayTradeService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayTradeService.java new file mode 100644 index 0000000..a7bc96f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/PayTradeService.java @@ -0,0 +1,136 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.enums.CoinType; +import com.sonic.lion.domain.enums.TradeEvent; +import com.sonic.lion.domain.input.CheckoutInput; +import com.sonic.lion.domain.input.PlatformGiftInput; +import com.sonic.lion.domain.input.PrePaymentInput; +import com.sonic.lion.domain.output.PrePaymentOutput; +import com.sonic.lion.domain.output.TradeHandleOutput; +import com.sonic.lion.domain.req.RefundReq; +import com.sonic.lion.domain.resp.CheckoutResp; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.TradeStatus; +import org.springframework.lang.NonNull; + +/** + * 交易 + */ +public interface PayTradeService { + + /** + * 根据tradeNo获取PayTrade + * + * @param tradeNo + * @return + */ + PayTrade getByTradeNo(String tradeNo); + + /** + * 预支付 + * + * @param input + * @return + */ + PrePaymentOutput prePayment(@NonNull PrePaymentInput input); + + /** + * 收银台确认支付 + * + * @param input + */ + CheckoutResp checkout(@NonNull CheckoutInput input); + + + /** + * 渠道处理成功后系统内处理 + * + * @param payTrade + * @param tradeEvent + */ + TradeHandleOutput tradeHandle(@NonNull PayTrade payTrade, @NonNull TradeEvent tradeEvent); + + /** + * 渠道处理成功后系统内处理 + * + * @param payChannel + * @param tradeNo + * @param tradeEvent + */ + TradeHandleOutput tradeHandleV2(PayChannel payChannel, @NonNull String tradeNo, @NonNull TradeEvent tradeEvent); + + /** + * 退款 + * + * @param req + */ + void refund(@NonNull RefundReq req); + + /** + * 通过平台和外部订单号查询 + * + * @param platform + * @param outTradeNo + * @return + */ + PayTrade getByPlatformAndOuterTradeNo(String platform, String outTradeNo); + + /** + * 完成担保交易 + * + * @param platform + * @param outTradeNo + */ + void completeSecuredTrade(@NonNull String platform, @NonNull String outTradeNo); + + /** + * 关闭交易 + * + * @param platform + * @param outTradeNo + */ + void closeTrade(@NonNull String platform, @NonNull String outTradeNo); + + /** + * 批量关闭交易(交易关闭时间小于当前时间的交易) + * + * @return + */ + int closeWaitPayTrade(); + + /** + * 更新交易状态 + * + * @param id + * @param status + * @param expectStatus + */ + void updateStatus(Long id, TradeStatus status, TradeStatus expectStatus); + + /** + * 更新支付信息 + * + * @param input + * @param payTrade + * @param fee + */ + void updatePaymentInfo(CheckoutInput input, PayTrade payTrade, long fee); + + /** + * 获取待入帐转入可现的时间 + * + * @param userId + * @return + */ + int getToWithdrawableIncomeTime(Long userId, CoinType coinType); + + /** + * 平台赠送buff + * + * @param req + * @return + */ + PayTrade platformGift(PlatformGiftInput req); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/PreChargeHandlerService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/PreChargeHandlerService.java new file mode 100644 index 0000000..c57035b --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/PreChargeHandlerService.java @@ -0,0 +1,37 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.req.PreChargeReq; +import com.sonic.lion.domain.resp.PrePaymentResp; + +/** + * 充值预下单 + */ +public interface PreChargeHandlerService { + + /** + * 预下单 + * @param userId + * @param req + * @param ip + * @return + */ + PrePaymentResp preCharge(Long userId, PreChargeReq req, String ip); + + /** + * 苹果iap预下单 + * @param userId + * @param req + * @param ip + * @return + */ + PrePaymentResp iapPreCharge(Long userId, PreChargeReq req, String ip); + + /** + * 安卓Google预下单 + * @param userId + * @param req + * @param ip + * @return + */ + PrePaymentResp googlePreCharge(Long userId, PreChargeReq req, String ip); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingChargeService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingChargeService.java new file mode 100644 index 0000000..94abfa7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingChargeService.java @@ -0,0 +1,12 @@ +package com.sonic.lion.service; + +/** + * 充值中数据获取 + */ +public interface ProcessingChargeService { + + /** + * 处理充值中数据 + */ + void hand(); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingDisputeService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingDisputeService.java new file mode 100644 index 0000000..bc99a07 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingDisputeService.java @@ -0,0 +1,12 @@ +package com.sonic.lion.service; + +/** + * 处理争议 + */ +public interface ProcessingDisputeService { + + /** + * 处理争议 + */ + void hand(); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingWithdrawReviewService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingWithdrawReviewService.java new file mode 100644 index 0000000..269914b --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingWithdrawReviewService.java @@ -0,0 +1,13 @@ +package com.sonic.lion.service; + +/** + * 提现处理服务 + * + */ +public interface ProcessingWithdrawReviewService { + + /** + * 处理提现申请 + */ + void hand(); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingWithdrawService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingWithdrawService.java new file mode 100644 index 0000000..40aaf7b --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/ProcessingWithdrawService.java @@ -0,0 +1,13 @@ +package com.sonic.lion.service; + +/** + * 处理提现 + */ +public interface ProcessingWithdrawService { + + /** + * 处理提现 + */ + void hand(); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/ReceiptHandlerService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/ReceiptHandlerService.java new file mode 100644 index 0000000..d2916f0 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/ReceiptHandlerService.java @@ -0,0 +1,24 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.input.UploadReceiptInput; +import com.sonic.lion.domain.req.UploadReceiptReq; + +/** + * 收据处理器 + */ +public interface ReceiptHandlerService { + + /** + * 处理收据上传 + * @param userId + * @param input + */ + void uploadIosSubReceipt(Long userId, UploadReceiptInput input); + + /** + * 处理Google收据上传 + * @param userId + * @param input + */ + void uploadGoogleSubReceipt(Long userId, UploadReceiptReq input); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/RefundService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/RefundService.java new file mode 100644 index 0000000..d1a5f9f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/RefundService.java @@ -0,0 +1,53 @@ +package com.sonic.lion.service; + +import com.sonic.common.rpc.Page; +import com.sonic.lion.domain.entity.AppleRefundRecord; +import com.sonic.lion.domain.entity.PayCallChannelRecord; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.entity.ProcessingDispute; +import com.sonic.lion.domain.req.RefundQueryReq; + +import java.time.LocalDateTime; + +/** + * 退款 服务 + * 主动查询退款 并记录 + */ +public interface RefundService { + + /** + * 拉取第三方 的争议数据 并写入 apple_refund_record 表中 + * @param startTime + */ + void pull(LocalDateTime startTime); + + /** + * 分页查询 退款的列表数据 + * @param refundQueryReq + * @return + */ + Page list(RefundQueryReq refundQueryReq); + + /** + * 已废弃 + * @param recordId + */ + @Deprecated + void paypalRefundByRecordId(Long recordId); + + /** + * 尝试退款 + * @param payTrade + * @param payCallChannelRecord + */ + void tryRefund(PayTrade payTrade, PayCallChannelRecord payCallChannelRecord); + + /** + * 扫描PayPal争议找不到渠道调用记录的数据 并写入apple_refund_record表中 + * 【在渠道表中找不到调用记录的数据会写到 t_processing_dispute 表中被定时扫描到】 + * @param processingDispute + * @return + */ + boolean processingDisupte(ProcessingDispute processingDispute); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/StripeSubscribeService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/StripeSubscribeService.java new file mode 100644 index 0000000..4668b4c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/StripeSubscribeService.java @@ -0,0 +1,23 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.bo.WebhookBo; +import com.sonic.lion.domain.input.CreateSubscribeCheckSessionInput; +import com.sonic.lion.domain.output.CreateSubscribeCheckoutSessionOutput; + +public interface StripeSubscribeService { + + /** + * 创建客户订阅结帐会话并生成支付url + * @param userId + * @param input + * @return + */ + CreateSubscribeCheckoutSessionOutput createSubscribeCheckoutSession(Long userId, CreateSubscribeCheckSessionInput input); + + /** + * 订阅处理 + * @param notifyId + * @param webhookBo + */ + void subscriptionHandler(Long notifyId, WebhookBo webhookBo); +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/UserSubscriptionService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/UserSubscriptionService.java new file mode 100644 index 0000000..4771d7d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/UserSubscriptionService.java @@ -0,0 +1,142 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.bo.SubscriptionBo; +import com.sonic.lion.domain.entity.UserSubscriptionNotify; +import com.sonic.lion.domain.entity.UserSubscription; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author code + */ +public interface UserSubscriptionService { + + /** + * 绑定 stripe 订阅 和 续订 + * @param subscriptionBo + */ + void bindStripe(SubscriptionBo subscriptionBo); + + /** + * 订阅 或 续订 + * + * @param subscriptionId + * @param platform + * @param exptime + * @param purchaseDate + * @param productId + */ + default void bind(String subscriptionId, UserSubscription.Platform platform, LocalDateTime exptime, LocalDateTime purchaseDate, String productId) { + bind(subscriptionId, platform, exptime, purchaseDate, productId, null, null, null); + } + + /** + * 订阅 或 续订 + * @param subscriptionId + * @param platform + * @param exptime + * @param purchaseDate + * @param productId + * @param userId + * @param purchaseToken + * @param ip + */ + void bind(String subscriptionId, UserSubscription.Platform platform, LocalDateTime exptime, LocalDateTime purchaseDate, String productId, Long userId, String purchaseToken, String ip); + + /** + * 根据订阅ID 获取 最近的一次 订阅。 + * + * @param subscriptionId + * @return + */ + UserSubscription getBySubscriptionId(String subscriptionId); + + /** + * 增加回调记录 + * + * @param subscriptionId + * @param content + * @param type + */ + Long addSubscriptionNotify(String subscriptionId, String messageId, String content, String type, UserSubscriptionNotify.Platform platform); + + /** + * 增加回调记录 + * + * @param subscriptionId + * @param content + * @param type + */ + Long addSubscriptionNotifyV2(String subscriptionId, String messageId, String content, String extend, String type, UserSubscriptionNotify.Platform platform); + + /** + * google token 设置 + * + * @param id + * @param token + */ + void setPurchaseToken(Long id, String token); + + /** + * 退订 + * + * @param subscriptionId + * @param platform + * @param expTime + */ + void cancel(String subscriptionId, UserSubscription.Platform platform, Long expTime); + + /** + * 完成记录 通知日志 + * + * @param notifyId + */ + void completeNotify(Long notifyId); + + /** + * 完成记录 通知日志 + * @param notifyId + * @param subscriptionId + */ + void updateStatusAndSubscriptionId(Long notifyId, String subscriptionId); + + /** + * 更新通知日志 extend + * @param notifyId + * @param appleRefundRecordId + * @param extend + */ + void updateNotifyExtend(Long notifyId, Long appleRefundRecordId, String extend); + + /** + * 通知日志 处理失败 + * + * @param notifyId + */ + void failNotify(Long notifyId); + + /** + * 续期失败发送系统消息 + * + * @param subscriptionId + * @param platform + */ + void payFailSystemMessage(String subscriptionId, UserSubscription.Platform platform); + + /** + * 更新订阅状态 三方通知 + * @param subscriptionId + * @param platform + * @param productId + * @param autoRenew + */ + void updateStatusByThird(String subscriptionId, UserSubscription.Platform platform, String productId, Boolean autoRenew); + + /** + * 查询用户是否是会员 + * @param userIdList + * @return + */ + List queryUserIsSubscribe(List userIdList); +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/WebhookHandlerService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/WebhookHandlerService.java new file mode 100644 index 0000000..e01c7dd --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/WebhookHandlerService.java @@ -0,0 +1,20 @@ +package com.sonic.lion.service; + +import com.sonic.lion.domain.req.GoogleUploadReceiptReqV2; + +import javax.servlet.http.HttpServletRequest; + +public interface WebhookHandlerService { + + void googleHandler(GoogleUploadReceiptReqV2 req); + + void appleHandler(HttpServletRequest request) throws Exception; + + void stripePaymentHandler(String signature, String payload) throws Exception; + + void stripePayoutHandler(String signature, String payload) throws Exception; + + void stripeSubscriptionHandler(String signature, String payload) throws Exception; + + void stripeDisputeHandler(String signature, String payload) throws Exception; +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/WithdrawService.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/WithdrawService.java new file mode 100644 index 0000000..f6d9491 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/WithdrawService.java @@ -0,0 +1,68 @@ +package com.sonic.lion.service; + +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.entity.WithdrawRequest; +import com.sonic.lion.domain.output.ChannelPayoutOutput; +import org.springframework.lang.NonNull; + +/** + * @author code + * 提现服务 + * 专门为提现 设计 + */ +public interface WithdrawService { + + + /** + * 检查提现金额 + * @param payChannel + * @param amount + */ + void checkAmount(PayChannel payChannel, Long amount,Long userId); + + + /** + * 发起提现审核 + * + * @param input + */ + void withdrawReview(WithdrawRequest input); + + + /** + * 审核时间过了 进行提现操作 + * + * @param payTrade + */ + void handWithdrawWhenReviewTimeOut(PayTrade payTrade); + + + /** + * 提现失败回滚逻辑 + * + * @param tradeNo + * @param bizType + * @param message + */ + void withdrawFail(@NonNull String tradeNo, BizType bizType, String message); + + + /** + * 提现成功 操作 + * + * @param tradeNo + */ + void withdrawSuccess(@NonNull String tradeNo); + + + /** + * mock接口用于 将 提现中、提现成功 状态的单子处理为终态 + * @param tradeNo + * @param payoutOutput + */ + @Deprecated + void mockWithdrawToFinalState(String tradeNo, ChannelPayoutOutput payoutOutput); + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/AccountBuffAwaitingServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/AccountBuffAwaitingServiceImpl.java new file mode 100644 index 0000000..24018ea --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/AccountBuffAwaitingServiceImpl.java @@ -0,0 +1,164 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.dao.AccountBuffAwaitingDao; +import com.sonic.lion.domain.entity.AccountBuffAwaiting; +import com.sonic.lion.service.AccountBuffAwaitingService; +import com.sonic.lion.enums.ToastResultCode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author: code + * @date: 2025/07/17 + * @Description: + * @version: 1.0.0 + */ +@Service +public class AccountBuffAwaitingServiceImpl implements AccountBuffAwaitingService { + + @Autowired + private AccountBuffAwaitingDao accountBuffAwaitingDao; + + + @Transactional + @Override + public void save(@NonNull AccountBuffAwaiting accountBuffAwaiting) { + accountBuffAwaitingDao.insert(accountBuffAwaiting); + } + + @Override + public List listWillToIncome(Integer num) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(AccountBuffAwaiting::getStatus, AccountBuffAwaiting.Status.PENDING) + .eq(AccountBuffAwaiting::getFronzenStatus, AccountBuffAwaiting.FronzenStatus.UN_FRONZEN) + .gt(AccountBuffAwaiting::getToWithdrawableIncomeTime, LocalDateTime.now().plusDays(-3)) + .lt(AccountBuffAwaiting::getToWithdrawableIncomeTime, LocalDateTime.now()).last(" limit " + num); + return accountBuffAwaitingDao.selectList(queryWrapper); + } + + @Override + public AccountBuffAwaiting getByTradeNo(String tradeNo) { + if (tradeNo == null) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(AccountBuffAwaiting::getTradeNo, tradeNo); + return accountBuffAwaitingDao.selectOne(queryWrapper); + } + + /** + * 查询待入账状态的 数据 + * + * @param tradeNo + * @return + */ + @Override + public List getListByTradeNo(String tradeNo) { + if (tradeNo == null) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(AccountBuffAwaiting::getStatus, AccountBuffAwaiting.Status.PENDING) + .eq(AccountBuffAwaiting::getTradeNo, tradeNo); + return accountBuffAwaitingDao.selectList(queryWrapper); + } + + @Transactional + @Override + public void updateStatus(@NonNull Long id, @NonNull AccountBuffAwaiting.Status status, AccountBuffAwaiting.Status expectStatus) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .eq(AccountBuffAwaiting::getId, id); + + if (expectStatus != null) { + updateWrapper.ge(AccountBuffAwaiting::getStatus, expectStatus); + } + + AccountBuffAwaiting updater = AccountBuffAwaiting.builder() + .status(status) + .build(); + int n = accountBuffAwaitingDao.update(updater, updateWrapper); + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(n != 1); + } + + @Transactional + @Override + public void updateAwaitingBuff(Long id, Long awaitingBuff, AccountBuffAwaiting.Status expectStatus) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .eq(AccountBuffAwaiting::getId, id); + + if (expectStatus != null) { + updateWrapper.ge(AccountBuffAwaiting::getStatus, expectStatus); + } + + AccountBuffAwaiting updater = AccountBuffAwaiting.builder() + .buff(awaitingBuff) + .build(); + int n = accountBuffAwaitingDao.update(updater, updateWrapper); + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(n != 1); + } + + /** + * 冻结 + * + * @param tradeNoList + */ + @Transactional + @Override + public void fronzenBuffAwaiting(List tradeNoList) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .in(!CollectionUtils.isEmpty(tradeNoList), AccountBuffAwaiting::getTradeNo, tradeNoList) + .eq(AccountBuffAwaiting::getFronzenStatus, AccountBuffAwaiting.FronzenStatus.UN_FRONZEN); + AccountBuffAwaiting updater = AccountBuffAwaiting.builder() + .fronzenStatus(AccountBuffAwaiting.FronzenStatus.FRONZEN) + .build(); + int n = accountBuffAwaitingDao.update(updater, updateWrapper); + } + + /** + * 解冻 + * + * @param tradeNoList + */ + @Transactional + @Override + public void unFronzenBuffAwaiting(List tradeNoList) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .in(!CollectionUtils.isEmpty(tradeNoList), AccountBuffAwaiting::getTradeNo, tradeNoList) + .eq(AccountBuffAwaiting::getFronzenStatus, AccountBuffAwaiting.FronzenStatus.FRONZEN); + AccountBuffAwaiting updater = AccountBuffAwaiting.builder() + .fronzenStatus(AccountBuffAwaiting.FronzenStatus.UN_FRONZEN) + .build(); + int n = accountBuffAwaitingDao.update(updater, updateWrapper); + } + + @Transactional + @Override + public boolean deleteById(Long id) { + return accountBuffAwaitingDao.deleteById(id) > 0; + } + + + @Transactional + @Override + public int updateOldData() { + return accountBuffAwaitingDao.updateOldData(); + } + + @Override + public int countMerchantSubscribeAwaitingIncome(String tradeNo,Long accountId) { + return accountBuffAwaitingDao.selectCount(Wrappers.lambdaQuery() + .eq(AccountBuffAwaiting::getTradeNo, tradeNo) + .eq(AccountBuffAwaiting::getAccountId, accountId) + .eq(AccountBuffAwaiting::getStatus, AccountBuffAwaiting.Status.PENDING) + ); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/AccountBuffBillServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/AccountBuffBillServiceImpl.java new file mode 100644 index 0000000..4a55137 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/AccountBuffBillServiceImpl.java @@ -0,0 +1,302 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Page; +import com.sonic.daosupport.converter.PageConverter; +import com.sonic.lion.dao.AccountBuffAwaitingDao; +import com.sonic.lion.dao.AccountBuffBillDao; +import com.sonic.lion.domain.entity.AccountBuff; +import com.sonic.lion.domain.entity.AccountBuffBill; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.enums.BuffAmountTypeEnum; +import com.sonic.lion.domain.enums.BuffClassifyEnum; +import com.sonic.lion.domain.enums.BuffType; +import com.sonic.lion.domain.enums.InOrOut; +import com.sonic.lion.domain.output.FilterTypeDict; +import com.sonic.lion.domain.req.BillListReq; +import com.sonic.lion.domain.resp.BillOutput; +import com.sonic.lion.domain.resp.SummaryBillListResp; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayGenrtatorCodeType; +import com.sonic.lion.service.AccountBuffBillService; +import com.sonic.lion.service.CommonSendMqService; +import com.sonic.lion.service.PayTradeService; +import com.sonic.lion.utils.KeyGenerator; +import com.sonic.lion.utils.NoDisplayBizNumSet; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; + +/** + * @author: code + * @date: 2025/07/17 + * @Description: + * @version: 1.0.0 + */ +@Service +@Slf4j +public class AccountBuffBillServiceImpl extends ServiceImpl implements AccountBuffBillService { + + @Autowired + private AccountBuffBillDao accountBuffBillDao; + @Autowired + private AccountBuffAwaitingDao accountBuffAwaitingDao; + @Autowired + private PayTradeService payTradeService; + @Autowired + private CommonSendMqService commonSendMqService; + /** + * 流水最大的页数 + */ + private final Integer MAX_PAGE_NUM = 500; + + @Override + public SummaryBillListResp billListSummary(BillListReq req, Session session) { + //校验分页参数,如果大于500页则不再继续分了 + if (req.getPage().getPn() > MAX_PAGE_NUM) { + req.getPage().setPn(MAX_PAGE_NUM); + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(AccountBuffBill::getUid, session.getUserId()) + .orderByDesc(AccountBuffBill::getCreateTime); + if (req.getType() == BillListReq.Type.BALANCE) { + queryWrapper.eq(AccountBuffBill::getBuffType, BuffType.BALANCE); + } else if (req.getType() == BillListReq.Type.INCOME) { + queryWrapper.in(AccountBuffBill::getBuffType, BuffType.WITHDRAWABLE_INCOME, BuffType.AWAITING_INCOME, BuffType.FROZEN_INCOME); + } + //只能查询最近1年的数据 + LocalDateTime startTime = LocalDateTime.now().minusYears(1); + LocalDateTime endTime = LocalDateTime.now(); + if (req.getStartTime() != null) { + LocalDateTime userStartTime = LocalDateTime.ofEpochSecond(req.getStartTime(), 0, ZoneOffset.ofHours(8)); + if (userStartTime.isAfter(startTime)) { + startTime = userStartTime; + } + } + if (req.getEndTime() != null) { + LocalDateTime userEndTime = LocalDateTime.ofEpochSecond(req.getEndTime(), 0, ZoneOffset.ofHours(8)); + if (userEndTime.isBefore(endTime)) { + endTime = userEndTime; + } + } + queryWrapper.between(AccountBuffBill::getCreateTime, startTime, endTime.plusSeconds(3)); + + Page page = req.getPage(); + IPage result = page(PageConverter.buildQueryPage(page), queryWrapper); + Page pageResult = PageConverter.convert(result, bill -> { + BillOutput resp = getBillListResp(session, bill); + return resp; + }); + + SummaryBillListResp resp = summary(req, session.getUserId(), pageResult); + return resp; + } + + /** + * 获取Bill list Resp + * + * @param session + * @param bill + * @return + */ + private BillOutput getBillListResp(Session session, AccountBuffBill bill) { + BillOutput resp = new BillOutput(); + resp.setId(bill.getId()); + resp.setAmount(bill.getBuff()); + resp.setPlatform(bill.getPlatform()); + resp.setTradeNo(bill.getTradeNo()); + //流水item显示名称 + resp.setItem(getItemDesc(bill)); + resp.setTime(bill.getCreateTime()); + resp.setPayment(bill.getPayment()); + //流水Order + resp.setBizNum(getBizNum(bill)); + resp.setInOrOut(bill.getInOrOut()); + resp.setBizType(bill.getBizType()); + resp.setStatus(bill.getStatus()); + resp.setMessage(bill.getReason()); + resp.setBuffType(bill.getBuffType()); + resp.setExtend(bill.getExtend()); + + //待收入转可提现的时间 + if (BuffType.AWAITING_INCOME.equals(bill.getBuffType()) && InOrOut.IN.equals(bill.getInOrOut())) { + resp.setToWithdrawableIncomeTime(bill.getToWithdrawableIncomeTime()); + } + return resp; + } + + /** + * 获取业务单号 + * + * @param bill + * @return + */ + private String getBizNum(AccountBuffBill bill) { + if (bill.getBizType() == BizType.REFUND) { + //退款时如果是退订单打赏则展示关联单号 + return StringUtils.isNotEmpty(bill.getBizNoRelationNo()) ? bill.getBizNoRelationNo() : bill.getBizNo(); + } else { + //如果业务类型时充值或体现则不返回 交易号 + return NoDisplayBizNumSet.tradeNoSets.contains(bill.getBizType()) ? null : bill.getBizNo(); + } + } + + private String getItemDesc(AccountBuffBill buffBill) { + return NoDisplayBizNumSet.getItemDesc(buffBill, false); + } + + + + /** + * 总结 + * + * @param billListReq + * @param userId + * @param page + * @param excludCharge + * @return + */ + @Override + public SummaryBillListResp summary(BillListReq billListReq, Long userId, Page page, boolean excludCharge) { +// LocalDateTime startTime = LocalDateTime.ofEpochSecond(billListReq.getStartTime(), 0, ZoneOffset.ofHours(8)); +// LocalDateTime endTime = LocalDateTime.ofEpochSecond(billListReq.getEndTime(), 0, ZoneOffset.ofHours(8)); +// +// String type = billListReq.getType() == null ? null : billListReq.getType().name(); +// Long totalIncome = 0L; +// Long totalOutcome = 0L; +// Integer bizType = null; +// if (excludCharge) { +// totalIncome = this.getBaseMapper().sumTotal(startTime, endTime, userId, InOrOut.IN.getValue(), type, "INCOME"); +// totalOutcome = this.getBaseMapper().sumTotal(startTime, endTime, userId, InOrOut.OUT.getValue(), type, "OUTCOME"); +// } else { +// totalIncome = this.getBaseMapper().sumTotal(startTime, endTime, userId, InOrOut.IN.getValue(), type, null); +// totalOutcome = this.getBaseMapper().sumTotal(startTime, endTime, userId, InOrOut.OUT.getValue(), type, null); +// } + + SummaryBillListResp summaryBillListResp = new SummaryBillListResp(); + summaryBillListResp.setIncomeTotal(0L); + summaryBillListResp.setOutcomeTotal(0L); + summaryBillListResp.setPageList(page); + return summaryBillListResp; + } + + + /** + * 专为 提现写的 bill 表 + * 这个bill表有状态 审核中 , 提现中 , 提现成功 , 提现失败 + * + * @param input + * @param accountBuff + * @return + */ + @Transactional(rollbackFor = Exception.class) + @Override + public Long insertBillWhenWithdraw(PayTrade input, AccountBuff accountBuff) { + Long total = accountBuff.getBalance() + accountBuff.getWithdrawableIncome() + accountBuff.getAwaitingIncome() + accountBuff.getFrozenIncome(); + //操作后总buff + //5.19.0 操作后总buff不变只是在 内部流转。 + long afterTotalBuff = total; + AccountBuffBill bill = AccountBuffBill.builder() + .platform(input.getPlatform()) + .billNo(KeyGenerator.instance().generatorUniqueKey(PayGenrtatorCodeType.ACCOUNT_BILL)) + .accountId(accountBuff.getId()) + .uid(input.getSrcAccountId()) + .desUid(null) + .buff(input.getOccurAmount()) + .bizType(input.getBizType()) + .payChannel(input.getPayChannel()) + .tradeNo(input.getTradeNo()) + .bizNo(input.getOutTradeNo()) + .bizNoRelationNo(input.getOutTradeNoRelationNo()) + .balance(afterTotalBuff) + .inOrOut(InOrOut.OUT) + .buffType(BuffType.BALANCE) + .reason(input.getErrorMessage()) + .withdrawStatus(AccountBuffBill.WithdrawStatus.IN_REVIEW) + .createTime(input.getCreateTime()) + .buffClassify(BuffClassifyEnum.getBuffClassify(input.getBizType(), InOrOut.OUT)) + .build(); + accountBuffBillDao.insert(bill); + return bill.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void insertBillWhenWithdrawFail(Long id, AccountBuff accountBuff) { + AccountBuffBill accountBuffBill = getById(id); + accountBuffBill.setId(null); + accountBuffBill.setInOrOut(InOrOut.IN); + accountBuffBill.setBillNo(KeyGenerator.instance().generatorUniqueKey(PayGenrtatorCodeType.ACCOUNT_BILL)); + accountBuffBill.setReason(null); + //写入当前用户的余额数据 + accountBuffBill.setBalance(accountBuff.getBalance()); + accountBuffBill.setWithdrawStatus(AccountBuffBill.WithdrawStatus.WITHDRAW_FAIL_BACK); + accountBuffBill.setBuffClassify(BuffClassifyEnum.getBuffClassify(BizType.WITHDRAW, InOrOut.IN)); + accountBuffBill.setCreateTime(LocalDateTime.now()); + accountBuffBill.setEditTime(LocalDateTime.now()); + accountBuffBillDao.insert(accountBuffBill); + } + + /** + * 更新流水状态 和 原因。 + * + * @param billId + * @param after + * @param message + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateWithdrawStatus(Long billId, AccountBuffBill.WithdrawStatus after, String message) { + return accountBuffBillDao.updateWithdrawStatus(billId, after.name(), message) > 0; + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateWithdrawBillAmount(Long billId, Long amount) { + return accountBuffBillDao.updateWithdrawBillAmount(billId, amount) > 0; + } + + /** + * 获取筛选中Buff Balance Income类型列表 + * + * @param amountType + * @return + */ + private List getFilterTypeDicList(BuffAmountTypeEnum amountType) { + List filterTypeDictList = Lists.newArrayList(); + List buffClassifyList = BuffClassifyEnum.getBuffClassifyList(amountType); + for (BuffClassifyEnum buffClassifyEnum : buffClassifyList) { + FilterTypeDict filterTypeDict = new FilterTypeDict(); + filterTypeDict.setCode(buffClassifyEnum.name()); + filterTypeDict.setName(buffClassifyEnum.getDesc()); + filterTypeDictList.add(filterTypeDict); + } + return filterTypeDictList; + } + + @Override + public void updateBuffTypeToRefund(String tradeNo) { + List list = list(Wrappers.lambdaQuery().eq(AccountBuffBill::getTradeNo, tradeNo).eq(AccountBuffBill::getInOrOut, InOrOut.IN.getValue()).eq(AccountBuffBill::getBuffType, BuffType.AWAITING_INCOME.getValue())); + for (AccountBuffBill bill : list) { + //更新为退款类型 + update(Wrappers.lambdaUpdate() + .set(AccountBuffBill::getBuffType, BuffType.REFUND) + .eq(AccountBuffBill::getId, bill.getId()) + ); + } + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/AccountBuffServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/AccountBuffServiceImpl.java new file mode 100644 index 0000000..b4fe7ea --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/AccountBuffServiceImpl.java @@ -0,0 +1,560 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Lists; +import com.sonic.common.exception.SysException; +import com.sonic.lion.dao.AccountBuffBillDao; +import com.sonic.lion.dao.AccountBuffDao; +import com.sonic.lion.domain.entity.AccountBuff; +import com.sonic.lion.domain.entity.AccountBuffAwaiting; +import com.sonic.lion.domain.entity.AccountBuffBill; +import com.sonic.lion.domain.entity.AppleRefundRecord; +import com.sonic.lion.domain.enums.*; +import com.sonic.lion.domain.input.BuffChangeInput; +import com.sonic.lion.domain.input.BuffTransferInput; +import com.sonic.lion.domain.output.WalletOutput; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.PayGenrtatorCodeType; +import com.sonic.lion.enums.ToastResultCode; +import com.sonic.lion.service.AccountBuffAwaitingService; +import com.sonic.lion.service.AccountBuffBillService; +import com.sonic.lion.service.AccountBuffService; +import com.sonic.lion.service.PayTradeService; +import com.sonic.lion.utils.KeyGenerator; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author: code + * @date: 2025/07/16 + * @Description: + * @version: 1.0.0 + */ +@Service +@Slf4j +public class AccountBuffServiceImpl implements AccountBuffService { + + @Value("${stripe.withdrawFeeRate}") + private BigDecimal withdrawFeeRate; + + @Autowired + private AccountBuffDao accountBuffDao; + @Autowired + private AccountBuffBillDao accountBuffBillDao; + @Autowired + private AccountBuffBillService accountBuffBillService; + @Autowired + private AccountBuffAwaitingService accountBuffAwaitingService; + @Autowired + private PayTradeService payTradeService; + + @Override + public AccountBuff getByUid(Long uid) { + if (uid == null) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(AccountBuff::getUid, uid); + return accountBuffDao.selectOne(queryWrapper); + } + + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean frozenBalance(Long uid, Long buff) { + boolean success = accountBuffDao.frozenBalance(uid, buff) > 0; + if (success) { + AccountBuff accountBuff = getByUid(uid); + //操作后总buff + long afterTotalBuff = getCurrentTotalBuff(accountBuff); + AccountBuffBill bill = AccountBuffBill.builder() + .platform("1") + .billNo(KeyGenerator.instance().generatorUniqueKey(PayGenrtatorCodeType.ACCOUNT_BILL)) + .accountId(accountBuff.getId()) + .uid(accountBuff.getUid()) + .desUid(-1L) + .buff(buff) + .bizType(BizType.FROZEN_BALANCE) + .payChannel(PayChannel.BUFF) + .tradeNo("") + .bizNo("") + .bizNoRelationNo("") + .balance(afterTotalBuff) + .inOrOut(InOrOut.OUT) + .buffType(BuffType.BALANCE) + .reason("") + .buffClassify(BuffClassifyEnum.getBuffClassify(BizType.FROZEN_BALANCE, InOrOut.OUT)) + .build(); + accountBuffBillService.save(bill); + } + return success; + } + + + @Transactional(rollbackFor = Exception.class) + @Override + public AppleRefundRecord.FronzenStatus fronzenBalanceAndSendMessage(Long userId, Long getBuffAmount) { + AccountBuff accountBuff = getByUid(userId); + if (accountBuff.getBalance() >= getBuffAmount) { + frozenBalance(userId, getBuffAmount); //冻结 金额并 发送消息 + return AppleRefundRecord.FronzenStatus.DEC; + } else if (accountBuff.getBalance() > 0 && accountBuff.getBalance() < getBuffAmount) { + frozenBalance(userId, accountBuff.getBalance()); //冻结 金额并 发送消息 + return AppleRefundRecord.FronzenStatus.FRONZEN; + } else { + return AppleRefundRecord.FronzenStatus.INIT; + } + } + + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean unFrozenBalance(Long uid, Long buff) { + boolean success = accountBuffDao.unFrozenBalance(uid, buff) > 0; + if (success) { + AccountBuff accountBuff = getByUid(uid); + //操作后总buff + long afterTotalBuff = getCurrentTotalBuff(accountBuff); + AccountBuffBill bill = AccountBuffBill.builder() + .platform("1") + .billNo(KeyGenerator.instance().generatorUniqueKey(PayGenrtatorCodeType.ACCOUNT_BILL)) + .accountId(accountBuff.getId()) + .uid(accountBuff.getUid()) + .desUid(-1L) + .buff(buff) + .bizType(BizType.UNFROZEN_BALANCE) + .payChannel(PayChannel.BUFF) + .tradeNo("") + .bizNo("") + .bizNoRelationNo("") + .balance(afterTotalBuff) + .inOrOut(InOrOut.IN) + .buffType(BuffType.BALANCE) + .reason("") + .buffClassify(BuffClassifyEnum.getBuffClassify(BizType.FROZEN_BALANCE, InOrOut.IN)) + .build(); + accountBuffBillService.save(bill); + } + return success; + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public Long addAwaitingIncome(@NonNull BuffChangeInput input) { + if (input.getBuff() == 0) { + return null; + } + validInput(input); + AccountBuff accountBuff = getByUid(input.getUid()); + if (accountBuff == null) { + //先初始化该渠道资金账户 + accountBuff = initAccount(input.getUid()); + } + + int n = accountBuffDao.addAwaitingIncome(accountBuff.getId(), input.getBuff()); + //根据订单数量 决定 具体的待入账时间。 + int toWithdrawableIncomeTime = payTradeService.getToWithdrawableIncomeTime(input.getUid(), CoinType.BUFF); + + //记录内部账单 + input.setToWithdrawableIncomeTime(LocalDateTime.now().plusDays(toWithdrawableIncomeTime)); + Long billId = saveBill(input, accountBuff, InOrOut.IN, BuffType.AWAITING_INCOME); + + AccountBuffAwaiting accountBuffAwaiting = AccountBuffAwaiting.builder() + .billId(billId) + .accountId(accountBuff.getId()) + .tradeNo(input.getTradeNo()) + .buff(input.getBuff()) + .toWithdrawableIncomeTime(LocalDateTime.now().plusDays(toWithdrawableIncomeTime)) + .status(AccountBuffAwaiting.Status.PENDING) + .build(); + + accountBuffAwaitingService.save(accountBuffAwaiting); + return billId; + } + + @Transactional(rollbackFor = Exception.class) + public void decAwaitingIncome(@NonNull BuffChangeInput input) { + if (input.getBuff() == 0) { + return; + } + + validInput(input); + AccountBuffAwaiting accountBuffAwaiting = accountBuffAwaitingService.getByTradeNo(input.getTradeNo()); + //未找到待入账记录 + + ToastResultCode.DATA_NOT_EXITS.check(accountBuffAwaiting == null); + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(accountBuffAwaiting.getStatus() == AccountBuffAwaiting.Status.CREDITED); + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(accountBuffAwaiting.getStatus() == AccountBuffAwaiting.Status.REFUNDED); + + //WEB 5.16.0 看扣款的金额是否已扣完了,扣完了的话修改状态为退款,否则修改待入账的金额值 + Long awaitingBuff = accountBuffAwaiting.getBuff() - input.getBuff(); + + if (awaitingBuff > 0) { + accountBuffAwaitingService.updateAwaitingBuff(accountBuffAwaiting.getId(), awaitingBuff, AccountBuffAwaiting.Status.PENDING); + } else { + accountBuffAwaitingService.updateStatus(accountBuffAwaiting.getId(), AccountBuffAwaiting.Status.REFUNDED, AccountBuffAwaiting.Status.PENDING); + } + + AccountBuff accountBuff = accountBuffDao.selectByUid(input.getUid()); + + int n = accountBuffDao.decAwaitingIncome(accountBuff.getId(), input.getBuff()); + //待入账金额不足或其他异常情况 + ToastResultCode.INSUFFICIENT_BALANCE.check(n != 1); + //记录内部账单 + saveBill(input, accountBuff, InOrOut.OUT, BuffType.AWAITING_INCOME); + //卖家入账流水 如果全额退款,buff_type字段更新为退款类型 + if (input.getFullRefund() != null && input.getFullRefund()) { + accountBuffBillService.updateBuffTypeToRefund(input.getTradeNo()); + } + } + + @Transactional(rollbackFor = Exception.class) + public void addBalance(@NonNull BuffChangeInput input) { + if (input.getBuff() == 0) { + return; + } + //入参校验 + validInput(input); + //获取账号基础数据 + AccountBuff accountBuff = accountBuffDao.selectByUid(input.getUid()); + if (accountBuff == null) { + //先初始化该渠道资金账户 + accountBuff = initAccount(input.getUid()); + } + //增加钱包余额 + int n = accountBuffDao.addBalance(accountBuff.getId(), input.getBuff()); + //增加总充值金额 + if (BizType.CHARGE.equals(input.getBizType())) { + accountBuffDao.addRechargeTotal(accountBuff.getUid(), input.getBuff()); + } + //记录内部账单 + saveBill(input, accountBuff, InOrOut.IN, BuffType.BALANCE); + } + + @Transactional(rollbackFor = Exception.class) + public Long decBalance(@NonNull BuffChangeInput input) { + if (input.getBuff() == 0) { + return null; + } + //入参校验 + validInput(input); + //获取账号基础数据 + AccountBuff accountBuff = accountBuffDao.selectByUid(input.getUid()); + if (accountBuff == null) { + //先初始化该渠道资金账户 + accountBuff = initAccount(input.getUid()); + } + int n = accountBuffDao.decBalance(accountBuff.getId(), input.getBuff()); + //定义钱包余额不足时的异常码 + ToastResultCode.INSUFFICIENT_BALANCE.check(n != 1); + //记录内部账单 + Long billId = saveBill(input, accountBuff, InOrOut.OUT, BuffType.BALANCE); + return billId; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void awaitingIncomeToWithdrawableIncome(@NonNull AccountBuffAwaiting accountBuffAwaiting) { + ToastResultCode.DATA_STATUS_INCORRECT.check(accountBuffAwaiting.getStatus() != AccountBuffAwaiting.Status.PENDING); + //更新待入账为已入账 + accountBuffAwaitingService.updateStatus(accountBuffAwaiting.getId(), AccountBuffAwaiting.Status.CREDITED, AccountBuffAwaiting.Status.PENDING); + //用户总待待入账收入减,总可提现金额加 + int n = accountBuffDao.awaitingIncomeToWithdrawableIncome(accountBuffAwaiting.getAccountId(), accountBuffAwaiting.getBuff()); + ToastResultCode.INSUFFICIENT_BALANCE.check(n != 1); + //更新流水状态为为可提现收入 + Long billId = accountBuffAwaiting.getBillId(); + if (billId != null) { + accountBuffBillDao.updateWithdrawableIncome(BuffType.WITHDRAWABLE_INCOME.getValue(), billId); + } + //待入账金额不足或其他异常情况 + + } + + + @Transactional(rollbackFor = Exception.class) + public void decWithdrawableIncome(@NonNull BuffChangeInput input) { + if (input.getBuff() == 0) { + return; + } + + validInput(input); + + AccountBuff accountBuff = accountBuffDao.selectByUid(input.getUid()); + if (accountBuff == null) { + //先初始化该渠道资金账户 + accountBuff = initAccount(input.getUid()); + } + int n = accountBuffDao.decWithdrawableIncome(accountBuff.getId(), input.getBuff()); + ToastResultCode.INSUFFICIENT_BALANCE.check(n != 1); + + //减少可提现账户的 金额 进入 ongoing 账户。 + addWithdrawOnGoing(accountBuff.getId(), input.getBuff()); + //记录内部账单 + saveBill(input, accountBuff, InOrOut.OUT, BuffType.WITHDRAWABLE_INCOME); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List transfer(@NonNull BuffTransferInput input) { + //声明流水ID列表 + List billIdList = Lists.newArrayList(); + + BuffChangeInput buffChangeInput = BuffChangeInput.builder() + .platform(input.getPlatform()) + .bizType(input.getBizType()) + .tradeNo(input.getTradeNo()) + .bizNo(input.getBizNo()) + .errorMessage(input.getErrorMessage()) + .bizNoRelationNo(input.getBizNoRelationNo()) + .payChannel(input.getPayChannel()) + .payMethod(input.getPayMethod()) + .reason(input.getReason()) + .extend(input.getExtend()) + .fullRefund(input.getFullRefund()) + .build(); + + //先扣款 + if (input.getDecUid() != null) { + buffChangeInput.setUid(input.getDecUid()); + buffChangeInput.setDesUid(input.getAddUid()); + buffChangeInput.setBuff(input.getDecBuff()); + buffChangeInput.setTargetUserId(input.getDecTargetUserId()); + //校验扣款金额是否正确 + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(input.getDecBuffType() != BuffType.BALANCE && input.getDecBuff() == 0); + if (input.getDecBuffType() == BuffType.BALANCE) { + Long billId = decBalance(buffChangeInput); + if (billId != null) { + billIdList.add(billId); + } + } else if (input.getDecBuffType() == BuffType.AWAITING_INCOME) { + decAwaitingIncome(buffChangeInput); + } else if (input.getDecBuffType() == BuffType.WITHDRAWABLE_INCOME) { + decWithdrawableIncome(buffChangeInput); + } else { + throw new SysException("", "暂不之支持扣除的buff类型"); + } + } + + //再增加 + if (input.getAddUid() != null) { + buffChangeInput.setUid(input.getAddUid()); + buffChangeInput.setDesUid(input.getDecUid()); + buffChangeInput.setBuff(input.getAddBuff()); + buffChangeInput.setTargetUserId(input.getAddTargetUserId()); + buffChangeInput.setGiftAmount(input.getGiftAmount()); + if (input.getAddBuffType() == BuffType.BALANCE) { + addBalance(buffChangeInput); + } else if (input.getAddBuffType() == BuffType.AWAITING_INCOME) { + Long billId = addAwaitingIncome(buffChangeInput); + if (billId != null) { + billIdList.add(billId); + } + } else if (input.getAddBuffType() == BuffType.WITHDRAWABLE_INCOME) { + addWithdrawableIncome(buffChangeInput); + } else { + throw new SysException("", "暂不支持扣除的buff类型"); + } + } + + //给系统账号增加 + if (input.getAddSystemUid() != null) { + buffChangeInput.setUid(input.getAddSystemUid()); + buffChangeInput.setDesUid(input.getDecUid()); + buffChangeInput.setBuff(input.getAddSystemBuff()); + if (input.getAddSystemBuffType() == BuffType.BALANCE) { + addBalance(buffChangeInput); + } else if (input.getAddSystemBuffType() == BuffType.AWAITING_INCOME) { + Long billId = addAwaitingIncome(buffChangeInput); + if (billId != null) { + billIdList.add(billId); + } + } else if (input.getAddSystemBuffType() == BuffType.WITHDRAWABLE_INCOME) { + addWithdrawableIncome(buffChangeInput); + } else { + throw new SysException("", "暂不支持扣除的buff类型"); + } + } + + return billIdList; + } + + @Override + public Long calculateIncome(AccountBuff accountBuff) { + if (accountBuff == null) { + return 0L; + } + return accountBuff.getWithdrawableIncome() + accountBuff.getAwaitingIncome() + accountBuff.getFrozenIncome(); + } + + + /** + * 这个方法仅 用于 提现回滚 + * + * @param input + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void addWithdrawableIncome(@NonNull BuffChangeInput input) { + if (input.getBuff() == 0) { + return; + } + validInput(input); + AccountBuff accountBuff = accountBuffDao.selectByUid(input.getUid()); + if (accountBuff == null) { + //先初始化该渠道资金账户 + accountBuff = initAccount(input.getUid()); + } + int n = accountBuffDao.addWithdrawableIncome(accountBuff.getId(), input.getBuff()); + + ToastResultCode.INSUFFICIENT_BALANCE.check(n != 1); + //回滚账单状态为已经失败 作废,并增加原因 + rollbackBill(input.getTradeNo(), input.getErrorMessage()); + } + + private void rollbackBill(String tradeNo, String errorMessage) { + accountBuffBillDao.updateStatus(tradeNo, BillStatusEnum.ROLL_BACK.getValue(), errorMessage); + } + + private void validInput(BuffChangeInput input) { + ToastResultCode.AMOUNT_LESS_THAN0.check(input.getBuff() < 0); + ToastResultCode.TRADE_NO_BLANK.check(StringUtils.isBlank(input.getTradeNo())); + } + + private AccountBuff initAccount(Long uid) { + AccountBuff accountBuff = AccountBuff.builder() + .uid(uid) + .status(AccountBuff.Status.ENABLE) + .balance(0L) + .withdrawableIncome(0L) + .awaitingIncome(0L) + .frozenIncome(0L) + .withdrawOnGoing(0L) + .build(); + accountBuffDao.insert(accountBuff); + return accountBuff; + } + + private long getCurrentTotalBuff(AccountBuff accountBuff) { + return accountBuff.getBalance() + accountBuff.getWithdrawableIncome() + accountBuff.getAwaitingIncome() + accountBuff.getFrozenIncome() + accountBuff.getWithdrawOnGoing(); + } + + + @Transactional + @Override + public Long saveBill(BuffChangeInput input, AccountBuff accountBuff, InOrOut inOrOut, BuffType buffType) { + //操作后总buff + long afterTotalBuff = getCurrentTotalBuff(accountBuff); + log.info("afterTotalBuff:{},input:{}", afterTotalBuff, input); + if (inOrOut == InOrOut.IN) { + afterTotalBuff += input.getBuff(); + } else if (inOrOut == InOrOut.OUT) { + afterTotalBuff -= input.getBuff(); + } + log.info("afterTotalBuff2:{},input:{}", afterTotalBuff, input); + + BizType bizType = input.getBizType(); + AccountBuffBill bill = AccountBuffBill.builder() + .platform(input.getPlatform()) + .billNo(KeyGenerator.instance().generatorUniqueKey(PayGenrtatorCodeType.ACCOUNT_BILL)) + .accountId(accountBuff.getId()) + .uid(input.getUid()) + .desUid(input.getDesUid()) + .targetUserId(input.getTargetUserId()) + .buff(input.getBuff()) + .bizType(bizType) + .payChannel(input.getPayChannel()) + .tradeNo(input.getTradeNo()) + .bizNo(input.getBizNo()) + .bizNoRelationNo(input.getBizNoRelationNo()) + .balance(afterTotalBuff) + .inOrOut(inOrOut) + .buffType(buffType) + .giftAmount(input.getGiftAmount()) + .reason(input.getErrorMessage()) + .extend(input.getExtend()) + .payMethod(input.getPayMethod()) + .buffClassify(BuffClassifyEnum.getBuffClassify(bizType, inOrOut)) + .toWithdrawableIncomeTime(input.getToWithdrawableIncomeTime()) + .build(); + + + //如果是退款并且是支出,则更新buff_type为REFUND + boolean isRefund = BizType.REFUND.equals(bizType); + if (isRefund && InOrOut.OUT.equals(inOrOut)) { + bill.setBuffType(BuffType.REFUND); + } + accountBuffBillService.save(bill); + return bill.getId(); + } + + /** + * 加上在途余额 + * + * @param srcAccountId + * @param amount + * @return + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void addWithdrawOnGoing(Long srcAccountId, Long amount) { + accountBuffDao.addWithdrawOnGoing(srcAccountId, amount); + } + + + /** + * 扣减 在途余额 + * + * @param srcAccountId + * @param amount + * @return + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean decWithdrawOnGoing(Long srcAccountId, Long amount) { + return accountBuffDao.decWithdrawOnGoing(srcAccountId, amount) > 0; + } + + + /** + * 在途资金 回滚到 可提现余额 + * + * @param id + * @param amount + * @return + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean withdrawOnGoingRollback(Long id, Long amount) { + ToastResultCode.INSUFFICIENT_BALANCE.check(!decWithdrawOnGoing(id, amount)); + return accountBuffDao.addWithdrawableIncome(id, amount) > 0; + } + + @Override + public WalletOutput wallet(Long userId) { + WalletOutput output = new WalletOutput(); + //设置提现手续费的抽成比例 + output.setWdFeeRate(withdrawFeeRate); + AccountBuff accountBuff = getByUid(userId); + if (accountBuff != null) { + output.setBalance(accountBuff.getBalance()); + output.setIncome(calculateIncome(accountBuff)); + output.setRequestWithdraw(accountBuff.getWithdrawOnGoing()); + output.setWithdrawable(accountBuff.getWithdrawableIncome() == null ? 0 : accountBuff.getWithdrawableIncome()); + output.setAwaitingIncome(accountBuff.getAwaitingIncome()); + } + return output; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ActivityServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ActivityServiceImpl.java new file mode 100644 index 0000000..7557ddd --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ActivityServiceImpl.java @@ -0,0 +1,187 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.common.utils.DateConvertUtils; +import com.sonic.lion.dao.AccountBuffBillDao; +import com.sonic.lion.dao.BuffRewardRecordDao; +import com.sonic.lion.dao.PayTradeDao; +import com.sonic.lion.domain.entity.BuffRewardRecord; +import com.sonic.lion.domain.entity.PayCharge; +import com.sonic.lion.domain.enums.ProductRewardType; +import com.sonic.lion.service.ActivityService; +import com.sonic.lion.service.PayChargeService; +import com.sonic.lion.service.PayConfigService; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class ActivityServiceImpl implements ActivityService { + + @Autowired + private BuffRewardRecordDao buffRewardRecordDao; + + @Autowired + private AccountBuffBillDao accountBuffBillDao; + + @Autowired + private PayConfigService payConfigService; + + @Autowired + private PayTradeDao payTradeDao; + @Autowired + private PayChargeService payChargeService; + + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveRecord(BuffRewardRecord buffRewardRecord) { + buffRewardRecordDao.insert(buffRewardRecord); + } + + + /** + * 判断某个 session 的版本号是否 大于等于 某个版本 + * + * @param specifyVersion 某个版本号 + * @param sessionVerison 当前session版本号 + * @return + */ + public static boolean checkVersion(String sessionVerison, String specifyVersion) { + if (StringUtils.isBlank(sessionVerison)) { + return true; + } + String[] specifyVersionList = specifyVersion.split("\\."); + String[] sessionVersionList = sessionVerison.split("\\."); + if (specifyVersionList.length == 3 && sessionVersionList.length == 3) { + String s1 = String.format("%02d", Integer.valueOf(specifyVersionList[0])) + String.format("%02d", Integer.valueOf(specifyVersionList[1])) + String.format("%02d", Integer.valueOf(specifyVersionList[2])); + String s2 = String.format("%02d", Integer.valueOf(sessionVersionList[0])) + String.format("%02d", Integer.valueOf(sessionVersionList[1])) + String.format("%02d", Integer.valueOf(sessionVersionList[2])); + Integer specifyVersionValue = Integer.valueOf(s1); + Integer sessionVersionValue = Integer.valueOf(s2); + return sessionVersionValue >= specifyVersionValue; + } else { + return false; + } + } + + /** + * 获取 奖励类型 并领取 + * + * @param userId + * @param createTime + * @param productId + * @return + */ + @Transactional(rollbackFor = Exception.class) + @Override + public ProductRewardType getRewardTypeByUserId(Long userId, LocalDateTime createTime, String productId, String version) { + log.info("getRewardTypeByUserId当前版本号{}", version); + if(!checkVersion(version,"4.18.0")){ + return null; + } + + if (productId == null) { + return null; + } + + //6.23.0 版本新增,老的充值版本没有首冲送buff的逻辑了 + if(LocalDateTime.now().isAfter(LocalDateTime.of(2024, 06, 01, 00, 00, 00))) { + return null; + } + + int firstChargeRows = buffRewardRecordDao.selectCount(Wrappers.lambdaQuery() + .eq(BuffRewardRecord::getRewardType, ProductRewardType.FIRST_CHARGE).eq(BuffRewardRecord::getUid, userId) + ); + + + /** + * gg.epal.web_buff_1 + * gg.epal.buff_0002 + * gg.epal.buff_009 + */ + List productList = Lists.newArrayList("gg.epal.web_buff_1", "gg.epal.buff_0002", "gg.epal.buff_009"); + + + //查询是否有充值过(充值并支付不算) + int chargeRows = payTradeDao.countAnyChargeOrders(userId); + + //没有领取过 首单奖励 。 而且 当前也没下过单 才可以 + if (chargeRows == 0 && firstChargeRows == 0 && productList.contains(productId) && payConfigService.enableFirstOrderReward()) { + return ProductRewardType.FIRST_CHARGE; + } + + //查询当前时间是否有领取过 这个档位的 周五福利。 + int everyFridayRows = buffRewardRecordDao.selectCount(Wrappers.lambdaQuery().eq(BuffRewardRecord::getRewardDate, DateConvertUtils.toZone(createTime,"Asia/Shanghai","America/Los_Angeles").toLocalDate()) + .eq(BuffRewardRecord::getRewardType, ProductRewardType.EVERY_FRIDAY).eq(BuffRewardRecord::getUid, userId) + .eq(BuffRewardRecord::getProductId, productId) + ); + + + //没有领取过 可以领取 + if (DayOfWeek.FRIDAY.equals(DateConvertUtils.toZone(createTime,"Asia/Shanghai","America/Los_Angeles").getDayOfWeek()) && everyFridayRows == 0 && payConfigService.enableEveryFridayReward()) { + return ProductRewardType.EVERY_FRIDAY; + } + return null; + } + + + @Override + public List getRewardTypeByUserIdV2(Long userId, LocalDateTime createTime, List productIdList, String version) { + if (CollectionUtils.isEmpty(productIdList)) { + return null; + } + //查询出PST本月已使用的商品列表 + Integer yearMonthInt = com.sonic.lion.utils.DateConvertUtils.formatYearMonthToInt(DateConvertUtils.toZone(createTime,"Asia/Shanghai","America/Los_Angeles")); + //获取本月已使用的大额充值数量 + List usedProductIdList = buffRewardRecordDao.selectList(Wrappers.lambdaQuery() + .select(BuffRewardRecord::getProductId) + .eq(BuffRewardRecord::getYearMonthInt, yearMonthInt) + .in(BuffRewardRecord::getProductId, productIdList) + .eq(BuffRewardRecord::getRewardType, ProductRewardType.LARGE_PRODUCT) + .eq(BuffRewardRecord::getUid, userId)) + .stream().map(e -> e.getProductId()).collect(Collectors.toList()); + return usedProductIdList; + } + + @Override + public PayCharge getRewardTypeByUserIdV2(Long userId, LocalDateTime createTime, String productId, String version) { + //没有充值档位ID,不能进行赠送,直接快速返回 + if (StringUtils.isEmpty(productId)) { + return null; + } + //查询档位数据是否存在 + PayCharge payCharge = payChargeService.getByProductId(productId); + if(payCharge == null) { + return null; + } + //没有大额充送类型,不能进行赠送,直接快速返回 + if(!ProductRewardType.LARGE_PRODUCT.name().equals(payCharge.getBizType())) { + return null; + } + //判断PST本月大额充送是否已经使用过,已使用则不能进行赠送,直接快速返回 + Integer yearMonthInt = com.sonic.lion.utils.DateConvertUtils.formatYearMonthToInt(DateConvertUtils.toZone(createTime,"Asia/Shanghai","America/Los_Angeles")); + //获取本月已使用的大额充值数量 + int useCount = buffRewardRecordDao.selectCount(Wrappers.lambdaQuery() + .select(BuffRewardRecord::getProductId) + .eq(BuffRewardRecord::getYearMonthInt, yearMonthInt) + .in(BuffRewardRecord::getProductId, productId) + .eq(BuffRewardRecord::getRewardType, ProductRewardType.LARGE_PRODUCT) + .eq(BuffRewardRecord::getUid, userId)); + if(useCount > 0) { + return null; + } + //没有被限制,则返回需要进行赠送的充值档位基础数据 + return payCharge; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/BuffTransferServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/BuffTransferServiceImpl.java new file mode 100644 index 0000000..2e08633 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/BuffTransferServiceImpl.java @@ -0,0 +1,124 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.dao.AccountBuffDao; +import com.sonic.lion.domain.entity.AccountBuff; +import com.sonic.lion.domain.entity.AccountBuffBill; +import com.sonic.lion.domain.enums.BuffClassifyEnum; +import com.sonic.lion.domain.enums.BuffType; +import com.sonic.lion.domain.enums.InOrOut; +import com.sonic.lion.domain.input.BuffTransferExtend; +import com.sonic.lion.domain.input.BuffTransferTargetInput; +import com.sonic.lion.enums.PayGenrtatorCodeType; +import com.sonic.lion.enums.ToastResultCode; +import com.sonic.lion.service.AccountBuffBillService; +import com.sonic.lion.service.BuffTransferService; +import com.sonic.lion.utils.KeyGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +/** + * @author code + * 转账服务 + */ +@Service +public class BuffTransferServiceImpl implements BuffTransferService { + + @Autowired + private AccountBuffDao accountBuffDao; + @Autowired + private AccountBuffBillService accountBuffBillService; + + /** + * 扣减 来源的 Buff 余额 + * + * @param extend + * @param from + */ + private Long decFromBalance(BuffTransferExtend extend, BuffTransferTargetInput from) { + AccountBuff fromAccountBuff = getAndInitAccount(from.getUid()); + int n = accountBuffDao.decBalance(fromAccountBuff.getId(), from.getAmount()); + //定义钱包余额不足时的异常码 + ToastResultCode.INSUFFICIENT_BALANCE.check(n != 1); + //记录内部账单 + Long billId = saveBill(extend, fromAccountBuff, extend.getDesUid(), from.getAmount(), InOrOut.OUT, BuffType.BALANCE); + return billId; + } + + /** + * 保存 流水表 + * + * @param buffTransferExtend 额外信息 + * @param accountBuff 账户信息 + * @param amount 金额 + * @param inOrOut 加减 + * @param buffType 账户类型 + */ + public Long saveBill(BuffTransferExtend buffTransferExtend, AccountBuff accountBuff, Long from, Long amount, InOrOut inOrOut, BuffType buffType) { + //操作后总buff + long afterTotalBuff = accountBuff.getTotalAmount(); + if (inOrOut == InOrOut.IN) { + afterTotalBuff += amount; + } else if (inOrOut == InOrOut.OUT) { + afterTotalBuff -= amount; + } + AccountBuffBill bill = AccountBuffBill.builder() + .platform("Epal") + .billNo(KeyGenerator.instance().generatorUniqueKey(PayGenrtatorCodeType.ACCOUNT_BILL)) + .accountId(accountBuff.getId()) + .uid(accountBuff.getUid()) + .desUid(from) + .buff(amount) + .bizType(buffTransferExtend.getBizType()) + .extend(buffTransferExtend.getExtend()) + .payChannel(buffTransferExtend.getPayChannel()) + .tradeNo(buffTransferExtend.getTradeNo()) + .bizNo(buffTransferExtend.getBizNo()) + .bizNoRelationNo(buffTransferExtend.getBizNoRelationNo()) + .balance(afterTotalBuff) + .inOrOut(inOrOut) + .buffType(buffType) + .reason(buffTransferExtend.getReason()) + .createTime(LocalDateTime.now()) + .buffClassify(BuffClassifyEnum.getBuffClassify(buffTransferExtend.getBizType(), inOrOut)) + .build(); + accountBuffBillService.save(bill); + return bill.getId(); + } + + /** + * 获取账户 如果没有 新增一个 余额为 0 的账户 + * + * @param uid + * @return + */ + @Override + public AccountBuff getAndInitAccount(Long uid) { + AccountBuff accountBuff = accountBuffDao.selectByUid(uid); + if (accountBuff == null) { + //先初始化该渠道资金账户 + accountBuff = initAccount(uid); + } + return accountBuff; + } + + /** + * 初始化 账户 + * + * @param uid + * @return + */ + private AccountBuff initAccount(Long uid) { + AccountBuff accountBuff = AccountBuff.builder() + .uid(uid) + .status(AccountBuff.Status.ENABLE) + .balance(0L) + .withdrawableIncome(0L) + .awaitingIncome(0L) + .frozenIncome(0L) + .build(); + accountBuffDao.insert(accountBuff); + return accountBuff; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CashierServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CashierServiceImpl.java new file mode 100644 index 0000000..cd67953 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CashierServiceImpl.java @@ -0,0 +1,139 @@ +package com.sonic.lion.service.impl; + +import com.sonic.common.exception.BizExceptionUtils; +import com.sonic.common.exception.SysExceptionUtils; +import com.sonic.common.rpc.Result; +import com.sonic.lion.domain.entity.PayCallChannelRecord; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.enums.CallChannelStatus; +import com.sonic.lion.domain.input.CheckoutInput; +import com.sonic.lion.domain.output.SyncChannelOutput; +import com.sonic.lion.domain.req.CheckoutReq; +import com.sonic.lion.domain.req.TradeQueryReq; +import com.sonic.lion.domain.resp.CheckoutResp; +import com.sonic.lion.domain.resp.QueryResp; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PaymentType; +import com.sonic.lion.enums.ToastResultCode; +import com.sonic.lion.enums.TradeStatus; +import com.sonic.lion.service.CashierService; +import com.sonic.lion.service.PayCallChannelService; +import com.sonic.lion.service.PayConfigService; +import com.sonic.lion.service.PayTradeService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class CashierServiceImpl implements CashierService { + + @Autowired + private PayTradeService payTradeService; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private PayConfigService payConfigService; + @Autowired + private PayCallChannelService payCallChannelService; + @Autowired + private TradeHandler tradeHandler; + + @Override + public CheckoutResp checkout(Long userId, CheckoutReq req) { + SysExceptionUtils.check(req.getPaymentType() == PaymentType.CHANNEL && req.getPayChannel() == null, "", "渠道支付时payChannel不能为null"); + //支付渠道开关控制 + String errorMessage = stringRedisTemplate.opsForValue().get("charge:enable:tips:" + req.getPayChannel().getValue()); + BizExceptionUtils.check(StringUtils.isNotEmpty(errorMessage), "99990001", errorMessage); + + //校验渠道开关是否打开 + boolean channelSwitchBl = payConfigService.paymentChannelSwitchCheckPass(userId, req.getPayChannel()); + ToastResultCode.CHANNEL_NOT_OPEN.check(!channelSwitchBl); + + //结账扣款 + CheckoutInput input = CheckoutInput.builder() + .uid(userId) + .tradeNo(req.getTradeNo()) + .payChannel(req.getPayChannel()) + .paymentType(req.getPaymentType()) + .returnUrl(req.getReturnUrl()) + .cancelUrl(req.getCancelUrl()) + .build(); + return payTradeService.checkout(input); + } + + @Override + public Result query(TradeQueryReq req) { + QueryResp resp = new QueryResp(); + //查询渠道表是否存在 + PayCallChannelRecord payCallChannelRecord = payCallChannelService.get(req.getSubmitId()); + if (payCallChannelRecord == null) { + return Result.success(resp); + } + PayTrade payTrade = payTradeService.getByTradeNo(payCallChannelRecord.getTradeNo()); + if (payTrade == null) { + return Result.success(resp); + } + + if (StringUtils.isNotBlank(payTrade.getPaymentTradeNo())) { + PayTrade paymentTrade = payTradeService.getByTradeNo(payTrade.getPaymentTradeNo()); + if (paymentTrade != null && TradeStatus.WAITPAY.equals(paymentTrade.getStatus()) && TradeStatus.FINISHED.equals(payTrade.getStatus())) { + + ToastResultCode.GAME_PAY_FAIL.check(true); + } + } + + if (BizType.CHARGE.equals(payTrade.getBizType())) { + resp.setChargeAmount(payTrade.getAmount()); + } + resp.setTradeNo(payTrade.getTradeNo()); + resp.setTradeStatus(payTrade.getStatus()); + if (payTrade.getPaymentTradeNo() != null) { + PayTrade paymentTrade = payTradeService.getByTradeNo(payTrade.getPaymentTradeNo()); + resp.setPaymentTradeNo(paymentTrade.getTradeNo()); + resp.setPaymentTradeOrderNo(paymentTrade.getOutTradeNo()); + resp.setPaymentBizType(paymentTrade.getBizType()); + } + + //交易关闭 返回相关信息 + if (payTrade.getStatus().equals(TradeStatus.CLOSED)) { + String errorMessage =payTrade.getErrorMessage() ; + Result result = Result.error("00000000",errorMessage); + result.setContent(resp); + return result; + } + if (payTrade.getStatus().equals(TradeStatus.REFUNDED)) { + Result result = Result.error("00000000", payTrade.getErrorMessage()); + result.setContent(resp); + return result; + } + + //如果渠道调用状态是 处理中 同步查询调用状态 + if (payCallChannelRecord.getStatus().equals(CallChannelStatus.PROCESSING) || payCallChannelRecord.getStatus().equals(CallChannelStatus.INIT)) { + try{ + SyncChannelOutput syncChannelOutput = tradeHandler.syncOrder(payCallChannelRecord); + resp.setTradeStatus(CallChannelStatus.toTradeStatus(syncChannelOutput.getCallChannelStatus())); + if (resp.getTradeStatus().equals(TradeStatus.CLOSED)) { + Result result = Result.error("00000000", payTrade.getErrorMessage()); + result.setContent(resp); + return result; + } + }catch (Exception e){ + payTrade = payTradeService.getByTradeNo(payCallChannelRecord.getTradeNo()); + resp.setTradeNo(payTrade.getTradeNo()); + resp.setTradeStatus(payTrade.getStatus()); + if (payTrade.getPaymentTradeNo() != null) { + PayTrade paymentTrade = payTradeService.getByTradeNo(payTrade.getPaymentTradeNo()); + resp.setPaymentTradeNo(paymentTrade.getTradeNo()); + resp.setPaymentTradeOrderNo(paymentTrade.getOutTradeNo()); + resp.setPaymentBizType(paymentTrade.getBizType()); + } + return Result.success(resp); + } + } + return Result.success(resp); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ChannelBlacklistServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ChannelBlacklistServiceImpl.java new file mode 100644 index 0000000..34f9820 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ChannelBlacklistServiceImpl.java @@ -0,0 +1,57 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.dao.ChannelBlacklistDao; +import com.sonic.lion.domain.entity.ChannelBlacklist; +import com.sonic.lion.service.ChannelBlacklistService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +/** + * @Author code + * @Description 渠道黑名单列表 + * @Date 2024/1/23 15:06 + * @Version 1.0 + */ +@Slf4j +@Service +public class ChannelBlacklistServiceImpl implements ChannelBlacklistService { + + @Autowired + private ChannelBlacklistDao channelBlacklistDao; + + + @Override + public void addBlacklist(Long userId, String channelType) { + try { + if(userId == null || StringUtils.isEmpty(channelType)) { + return; + } + ChannelBlacklist channelBlacklist = channelBlacklistDao.getPayerId(userId, channelType); + if(channelBlacklist != null) { + //更新数据 + ChannelBlacklist updateChannelBlacklist = new ChannelBlacklist(); + updateChannelBlacklist.setId(channelBlacklist.getId()); + updateChannelBlacklist.setIsDelete(false); + updateChannelBlacklist.setBlockCount(channelBlacklist.getBlockCount() + 1); + updateChannelBlacklist.setEditTime(LocalDateTime.now()); + channelBlacklistDao.updateById(updateChannelBlacklist); + return; + } + //写入数据 + ChannelBlacklist saveChannelBlacklist = ChannelBlacklist. + builder().userId(userId) + .channelType(channelType) + .blockCount(1) + .isDelete(false) + .createTime(LocalDateTime.now()) + .editTime(LocalDateTime.now()).build(); + channelBlacklistDao.insert(saveChannelBlacklist); + } catch (Exception e) { + log.error("===> ChannelBlacklistService save channelBlacklist error : ", e); + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ChannelProcessingServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ChannelProcessingServiceImpl.java new file mode 100644 index 0000000..54bf37e --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ChannelProcessingServiceImpl.java @@ -0,0 +1,211 @@ +package com.sonic.lion.service.impl; + +import com.sonic.common.AppRuntime; +import com.sonic.common.utils.RedisLock; +import com.sonic.lion.dao.PayCallChannelRecordDao; +import com.sonic.lion.dao.PayTradeDao; +import com.sonic.lion.dao.ProcessingChargeDao; +import com.sonic.lion.dao.ProcessingWithdrawDao; +import com.sonic.lion.domain.entity.PayCallChannelRecord; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.entity.ProcessingCharge; +import com.sonic.lion.domain.entity.ProcessingWithdraw; +import com.sonic.lion.domain.enums.CallChannelStatus; +import com.sonic.lion.domain.output.ChannelCheckOutput; +import com.sonic.lion.domain.output.SyncChannelOutput; +import com.sonic.lion.domain.output.TradeHandleOutput; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.ToastResultCode; +import com.sonic.lion.enums.TradeStatus; +import com.sonic.lion.service.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicReference; + +@Service +@Slf4j +public class ChannelProcessingServiceImpl implements ChannelProcessingService { + + @Autowired + private PayCallChannelService payCallChannelService; + + @Autowired + private PayTradeService payTradeService; + + @Autowired + private PayTradeDao payTradeDao; + + @Autowired + private WithdrawService withdrawService; + + @Autowired + private RefundService refundService; + + @Autowired + private PayCallChannelRecordDao payCallChannelRecordDao; + + @Autowired + private ProcessingChargeDao processingChargeDao; + + + @Autowired + private ProcessingWithdrawDao processingWithdrawDao; + + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private AppRuntime appRuntime; + + @Override + public SyncChannelOutput chargeProcessing(PayCallChannelRecord record) { + AtomicReference atomicReference = new AtomicReference<>(); + //加锁,避免定时任务并发重复处理 + RedisLock redisLock = new RedisLock(appRuntime.buildPrefixKey("lock", "chargeProcessingHandler", record.getId()), redisWrapper); + redisLock.tryAcquireRun(10 * 60 * 1000, () -> { + atomicReference.set(chargeProcessingHandler(record)); + return true; + }); + return atomicReference.get(); + } + + /** + * 充值中的数据状态获取处理 + * @param record + * @return + */ + public SyncChannelOutput chargeProcessingHandler(PayCallChannelRecord record) { + //业务类型判断:非充值业务类型,不进行处理。直接抛出异常 + ToastResultCode.UNSUPPORTED_BUSINESS_TYPE.check(!BizType.CHARGE.equals(record.getBizType())); + //查询充值中的基础数据 + ProcessingCharge processingCharge = processingChargeDao.findByRecordId(record.getId()); + //数据不存在,则直接快速返回 + if(processingCharge == null) { + return null; + } + //判断数据状态是否正确(状态不为 初始化、处理中 则删掉 充值中的基础数据,并快速返回) + if (!CallChannelStatus.PROCESSING.equals(record.getStatus()) && !CallChannelStatus.INIT.equals(record.getStatus())) { + processingChargeDao.deleteById(processingCharge.getId()); + return SyncChannelOutput.builder().callChannelStatus(record.getStatus()).build(); + } + log.info("===> chargeProcessingHandler record :{}", record); + //从渠道侧 获取充值数据是否正确 + ChannelCheckOutput checkOutput = payCallChannelService.checkRecord(record); + log.info("===> chargeProcessingHandler checkOutput :{}", checkOutput); + //根据交易号获取交易基础数据 + PayTrade payTrade = payTradeService.getByTradeNo(record.getTradeNo()); + //判断交易状态,如果当前付款交易状态不为待处理、已关闭状态则直接删掉 充值中的基础数据 + if(TradeStatus.WAITPAY != payTrade.getStatus() && TradeStatus.CLOSED != payTrade.getStatus()) { + processingChargeDao.deleteById(processingCharge.getId()); + return SyncChannelOutput.builder().callChannelStatus(checkOutput.getStatus()).message(checkOutput.getErrorMessage()).build(); + } + //拉黑也是争议的一种 + if (checkOutput.isInBlackList() && checkOutput.getTransactionId() != null) { + //更新交易状态 + payTradeDao.updateStatusErrorMessage(CallChannelStatus.toTradeStatus(checkOutput.getStatus()).getValue(), checkOutput.getErrorMessage(), payTrade.getTradeNo()); + //进行退款操作 + refundService.tryRefund(payTrade, record); + //抛出异常 + ToastResultCode.UNSUPPORTED_BUSINESS_TYPE.check(true); + } + //订单已经过期 设置为过期() + if (record.getExpTime() != null && record.getExpTime().isBefore(LocalDateTime.now())) { + checkOutput.setStatus(CallChannelStatus.EXPIRED); + } + + //处理成功的时候 充值处理 + if (checkOutput.getStatus() == CallChannelStatus.SUCC) { + TradeHandleOutput tradeHandleOutput = payTradeService.tradeHandleV2(record.getChannel(), record.getTradeNo(), checkOutput.getChannelEvent()); + tradeHandleOutput.setChargeAmount(payTrade.getAmount()); + handWebsocket(payTrade, tradeHandleOutput); + //失败处理 + } else if (checkOutput.getStatus() == CallChannelStatus.FAIL) { + payTradeDao.updateStatusErrorMessage(CallChannelStatus.toTradeStatus(checkOutput.getStatus()).getValue(), checkOutput.getErrorMessage(), payTrade.getTradeNo()); + } else if (checkOutput.getStatus() == CallChannelStatus.PROCESSING) { + //处理中不做处理 + } else if (checkOutput.getStatus() == CallChannelStatus.CANCEL) { + //更新状态为取消 + payTradeDao.updateStatusErrorMessage(CallChannelStatus.toTradeStatus(checkOutput.getStatus()).getValue(), checkOutput.getErrorMessage(), payTrade.getTradeNo()); + } else if (checkOutput.getStatus() == CallChannelStatus.EXPIRED) { + payCallChannelRecordDao.updateStatus(record.getId(), CallChannelStatus.EXPIRED.getValue()); + //渠道调用已经失效 根据是否返回交易ID 决定是否退款 + if (StringUtils.isNotEmpty(checkOutput.getTransactionId())) { + try { + refundService.tryRefund(payTrade, record); + } catch (Exception e) { + payTradeDao.updateStatusErrorMessage(CallChannelStatus.toTradeStatus(checkOutput.getStatus()).getValue(), checkOutput.getErrorMessage(), payTrade.getTradeNo()); + } + } else { + payTradeDao.updateStatusErrorMessage(CallChannelStatus.toTradeStatus(checkOutput.getStatus()).getValue(), checkOutput.getErrorMessage(), payTrade.getTradeNo()); + } + } + //删除处理中的基础数据 + if (!CallChannelStatus.PROCESSING.equals(checkOutput.getStatus()) && !CallChannelStatus.INIT.equals(checkOutput.getStatus())) { + processingChargeDao.deleteById(processingCharge.getId()); + } + //构造输出对象 + return SyncChannelOutput.builder().callChannelStatus(checkOutput.getStatus()).message(checkOutput.getErrorMessage()).build(); + } + + @Override + public SyncChannelOutput withdrawProcessing(PayCallChannelRecord record) { + //同步的类型不是充值 不处理 + ToastResultCode.UNSUPPORTED_BUSINESS_TYPE.check(!BizType.WITHDRAW.equals(record.getBizType())); + //数据不存在,则直接快速返回 + ProcessingWithdraw processingWithdraw = processingWithdrawDao.findByRecordId(record.getId()); + if (processingWithdraw == null) { + return null; + } + //提现中的数据状态不为 初始化、处理中 则表明已经走到了终态,直接删除掉数据并快速返回 + if (!CallChannelStatus.PROCESSING.equals(record.getStatus()) && !CallChannelStatus.INIT.equals(record.getStatus())) { + processingWithdrawDao.deleteById(processingWithdraw.getId()); + return SyncChannelOutput.builder().callChannelStatus(record.getStatus()).build(); + } + //根据交易号查询交易基础数据 + PayTrade payTrade = payTradeService.getByTradeNo(record.getTradeNo()); + //从渠道侧 获取提现最终处理结果 + ChannelCheckOutput checkOutput = payCallChannelService.checkRecord(record); + //处理成功的时候 充值处理 + if (checkOutput.getStatus() == CallChannelStatus.SUCC) { + payTradeService.tradeHandleV2(record.getChannel(), record.getTradeNo(), checkOutput.getChannelEvent()); + withdrawService.withdrawSuccess(record.getTradeNo()); + //处理失败 返回异常消息, 提现如果失败 回滚资金 + } else if (checkOutput.getStatus() == CallChannelStatus.FAIL) { + withdrawService.withdrawFail(payTrade.getTradeNo(), payTrade.getBizType(), checkOutput.getErrorMessage()); + payTradeDao.updateStatusErrorMessage(CallChannelStatus.toTradeStatus(checkOutput.getStatus()).getValue(), checkOutput.getErrorMessage(), payTrade.getTradeNo()); + //更新处理中的 错误消息 + } else if (checkOutput.getStatus() == CallChannelStatus.PROCESSING) { + payTradeDao.updateErrorMessage(payTrade.getTradeNo(), checkOutput.getErrorMessage()); + } else if (checkOutput.getStatus() == CallChannelStatus.CANCEL) { + //暂不处理 + } + + if (!CallChannelStatus.PROCESSING.equals(checkOutput.getStatus()) && !CallChannelStatus.INIT.equals(checkOutput.getStatus())) { + processingWithdrawDao.deleteById(processingWithdraw.getId()); + } + return SyncChannelOutput.builder().callChannelStatus(checkOutput.getStatus()).message(checkOutput.getErrorMessage()).build(); + } + + + /** + * 处理消息 + * + * @param payTrade + * @param tradeHandleOutput + */ + private void handWebsocket(PayTrade payTrade, TradeHandleOutput tradeHandleOutput) { + try { + if (payTrade != null && StringUtils.isNotBlank(payTrade.getPaymentTradeNo())) { + PayTrade paymentTrade = payTradeService.getByTradeNo(payTrade.getPaymentTradeNo()); + tradeHandleOutput.setPaymentTradeNo(paymentTrade.getTradeNo()); + tradeHandleOutput.setPaymentTradeOrderNo(paymentTrade.getOutTradeNo()); + tradeHandleOutput.setPaymentBizType(paymentTrade.getBizType()); + } + } catch (Exception e) { + + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ChargeProductConfigServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ChargeProductConfigServiceImpl.java new file mode 100644 index 0000000..4b56725 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ChargeProductConfigServiceImpl.java @@ -0,0 +1,66 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.domain.entity.PayCharge; +import com.sonic.lion.domain.enums.ProductRewardType; +import com.sonic.lion.domain.output.ChargeProductConfigListOutput; +import com.sonic.lion.domain.output.ChargeProductConfigOutput; +import com.sonic.lion.service.ActivityService; +import com.sonic.lion.service.ChargeProductConfigService; +import com.sonic.lion.service.PayChargeService; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @Author code + * @Description 充值商品配置 + * @Date 2024/5/27 16:57 + * @Version 1.0 + */ +@Slf4j +@Service +public class ChargeProductConfigServiceImpl implements ChargeProductConfigService { + + @Autowired + private PayChargeService payChargeService; + @Autowired + private ActivityService activityService; + + @Override + public ChargeProductConfigOutput getRechargeLevelConfig(Long currentUserId, String platform, String version) { + ChargeProductConfigOutput output = new ChargeProductConfigOutput(); + //从数据库查询出配置的充值档位列表数据 + List list = payChargeService.list(platform, version); + List productOutputList = Lists.newArrayList(); + List largeProductIdList = list.stream() + .filter(e -> ProductRewardType.LARGE_PRODUCT.name().equals(e.getBizType())) + .map(e -> e.getProductId()).collect(Collectors.toList()); + //获取已得到赠送的大额充送的类型数据 + List rewardProductIdList = activityService.getRewardTypeByUserIdV2(currentUserId, LocalDateTime.now(), largeProductIdList, version); + + //循环数据中的配置基础数据 + for (PayCharge payCharge : list) { + ChargeProductConfigListOutput productOutput = new ChargeProductConfigListOutput(); + productOutput.setProductId(payCharge.getProductId()); + productOutput.setChargeAmount(payCharge.getChargeAmount()); + productOutput.setPayAmount(payCharge.getPayAmount()); + //当前业务类型为大额充送档位 且 本月没有享受过该档位的充送 + if(ProductRewardType.LARGE_PRODUCT.name().equals(payCharge.getBizType()) && !rewardProductIdList.contains(payCharge.getProductId())) { + //设置赠送金额 + productOutput.setGiftAmount(payCharge.getGiftAmount()); + } else if(ProductRewardType.HOT.name().equals(payCharge.getBizType())) { + //标记hot + productOutput.setHot(true); + } + productOutputList.add(productOutput); + } + output.setProductList(productOutputList); + return output; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CheckOutServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CheckOutServiceImpl.java new file mode 100644 index 0000000..9e7bb1d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CheckOutServiceImpl.java @@ -0,0 +1,101 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.input.CheckoutInput; +import com.sonic.lion.domain.input.PrePaymentInput; +import com.sonic.lion.domain.output.PrePaymentOutput; +import com.sonic.lion.domain.req.PrePaymentReq; +import com.sonic.lion.domain.resp.BuffCheckoutResp; +import com.sonic.lion.domain.resp.CheckoutResp; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.PaymentType; +import com.sonic.lion.enums.ToastResultCode; +import com.sonic.lion.enums.TradeStatus; +import com.sonic.lion.service.AccountBuffBillService; +import com.sonic.lion.service.CheckOutService; +import com.sonic.lion.service.PayTradeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import static com.sonic.lion.enums.PaymentType.BALANCE; + + +@Slf4j +@Service +public class CheckOutServiceImpl implements CheckOutService { + + @Autowired + private PayTradeService payTradeService; + @Autowired + private AccountBuffBillService accountBuffBillService; + + /** + * 充值且支付操作,充值成功后调用付款的方法 + * + * @param paymentTradeNo + * @param srcUid + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public void chargeAfterPayment(String paymentTradeNo, Long srcUid) { + log.info("充值并支付,充值后支付处理, paymentTradeNo: {}", paymentTradeNo); + PayTrade paymentTrade = payTradeService.getByTradeNo(paymentTradeNo); + if (paymentTrade == null || paymentTrade.getStatus() != TradeStatus.WAITPAY) { + return; + } + try { + CheckoutInput checkoutInput = CheckoutInput.builder() + .uid(srcUid) + .tradeNo(paymentTradeNo) + .paymentType(PaymentType.BALANCE) + .build(); + payTradeService.checkout(checkoutInput); + } catch (Exception e) { + log.warn("充值后支付处理扣款GAME异常", e); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public BuffCheckoutResp checkout(PrePaymentReq req) { + ToastResultCode.PARAM_ERROR.check(req.getDesAccountId() == null); + PrePaymentInput input = PrePaymentInput.builder() + .platform(req.getPlatform()) + .platformFee(req.getPlatformFee()) + .outTradeNo(req.getOutTradeNo()) + .outTradeNoRelationNo(req.getOutTradeNoRelationNo()) + .bizType(req.getBizType()) + .name(req.getName()) + .srcAccountId(req.getSrcAccountId()) + .desAccountNo(req.getDesAccountId().toString()) + .productAmount(req.getProductAmount()) + .promoAmount(req.getPromoAmount()) + .remark(req.getRemark()) + .srcAccountName("") + .closeTime(req.getCloseTime()) + .resourceKey(req.getResourceKey()) + .resourceNum(req.getResourceNum()) + .extend(req.getExtend()) + .payChannel(PayChannel.STRIPE) + .ip(req.getIp()) + .build(); + //结账预下单 + PrePaymentOutput prePaymentOutput = payTradeService.prePayment(input); + //结账扣款 + CheckoutInput checkoutInput = CheckoutInput.builder() + .uid(req.getSrcAccountId()) + .tradeNo(prePaymentOutput.getTradeNo()) + .payChannel(PayChannel.BUFF) + .paymentType(BALANCE) + .build(); + CheckoutResp checkoutResp = payTradeService.checkout(checkoutInput); + BuffCheckoutResp result = new BuffCheckoutResp(); + BeanUtils.copyProperties(checkoutResp, result); + return result; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CommonMessageServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CommonMessageServiceImpl.java new file mode 100644 index 0000000..e872672 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CommonMessageServiceImpl.java @@ -0,0 +1,43 @@ +package com.sonic.lion.service.impl; + + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.Maps; +import com.sonic.lion.service.CommonMessageService; +import com.sonic.pigeon.lib.client.MessageClient; +import com.sonic.pigeon.lib.enums.StationMessageTypeEnum; +import com.sonic.pigeon.lib.input.SendMessageInput; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 公共发送系统通知 + */ +@Service +public class CommonMessageServiceImpl implements CommonMessageService { + + @Autowired + private MessageClient messageClient; + + @Override + public void memberRenewSuccess(Long userId, LocalDateTime expireTime) { + String title = StationMessageTypeEnum.MEMBER_RENEW_SUCCESS.getTitle(); + String content = StationMessageTypeEnum.MEMBER_RENEW_SUCCESS.getContent(); + SendMessageInput sendMessageInput = new SendMessageInput(-1L, userId, StationMessageTypeEnum.MEMBER_RENEW_SUCCESS.getIndex(), title, content); + Map extras = Maps.newHashMap(); + extras.put("expireTime", expireTime); + sendMessageInput.setExtras(JSON.toJSONString(extras)); + messageClient.sendMessage(sendMessageInput); + } + + @Override + public void memberRenewFail(Long userId) { + String title = StationMessageTypeEnum.MEMBER_RENEW_FAIL.getTitle(); + String content = StationMessageTypeEnum.MEMBER_RENEW_FAIL.getContent(); + SendMessageInput sendMessageInput = new SendMessageInput(-1L, userId, StationMessageTypeEnum.MEMBER_RENEW_FAIL.getIndex(), title, content); + messageClient.sendMessage(sendMessageInput); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CommonSendMqServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CommonSendMqServiceImpl.java new file mode 100644 index 0000000..b15b321 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/CommonSendMqServiceImpl.java @@ -0,0 +1,18 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.service.CommonSendMqService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 发送消息到mq + * + * @Author code + * @Date 2022/3/2 + * @Version 1.0 + */ +@Slf4j +@Service +public class CommonSendMqServiceImpl implements CommonSendMqService { + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/FreeWithdrawConfigServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/FreeWithdrawConfigServiceImpl.java new file mode 100644 index 0000000..19e9055 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/FreeWithdrawConfigServiceImpl.java @@ -0,0 +1,261 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.common.utils.MessageUtils; +import com.sonic.lion.domain.req.FreeWithdrawReq; +import com.sonic.lion.enums.FreeWithdrawReason; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.dao.FreeWithdrawConfigDao; +import com.sonic.lion.dao.FreeWithdrawFeeBillDao; +import com.sonic.lion.domain.entity.FreeWithdrawConfig; +import com.sonic.lion.domain.entity.FreeWithdrawFeeBill; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.input.WithdrawFeeInput; +import com.sonic.lion.domain.output.WithdrawFeeReduceInfoOutput; +import com.sonic.lion.service.FreeWithdrawConfigService; +import com.sonic.lion.service.PayAccountFundThirdService; +import com.sonic.lion.service.PayConfigService; +import com.sonic.lion.enums.ToastResultCode; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class FreeWithdrawConfigServiceImpl implements FreeWithdrawConfigService { + + @Value("${stripe.withdrawFeeBase:25}") + private Long stripeWithdrawFeeBase; + + @Value("${stripe.withdrawFeeRate}") + private BigDecimal stripeWithdrawFeeRate; + + @Autowired + private FreeWithdrawConfigDao freeWithdrawConfigDao; + + @Autowired + private PayAccountFundThirdService payAccountFundThirdService; + + @Autowired + private FreeWithdrawFeeBillDao freeWithdrawFeeBillDao; + @Autowired + private PayConfigService payConfigService; + + @Transactional(rollbackFor = Exception.class) + @Override + public void setFreeWithdraw(FreeWithdrawReq freeWithdraw) { + FreeWithdrawConfig freeWithdrawConfig = freeWithdrawConfigDao.selectOne(Wrappers.lambdaQuery(). + eq(FreeWithdrawConfig::getReason, freeWithdraw.getReason()). + eq(FreeWithdrawConfig::getUid, freeWithdraw.getUid())); + if (freeWithdrawConfig == null) { + freeWithdrawConfig = FreeWithdrawConfig.builder() + .uid(freeWithdraw.getUid()) + .startTime(freeWithdraw.getStartTime()) + .endTime(freeWithdraw.getEndTime()) + .reason(freeWithdraw.getReason()) + .rate(freeWithdraw.getRate()) + .createTime(LocalDateTime.now()) + .editTime(LocalDateTime.now()).build(); + //只有开始时间不为空时才能够写入数据 + if(freeWithdrawConfig.getStartTime() != null) { + freeWithdrawConfigDao.insert(freeWithdrawConfig); + } + } else { + freeWithdrawConfig.setReason(freeWithdraw.getReason()); + freeWithdrawConfig.setStartTime(freeWithdraw.getStartTime() == null ? freeWithdrawConfig.getStartTime() : freeWithdraw.getStartTime()); + freeWithdrawConfig.setEndTime(freeWithdraw.getEndTime()); + freeWithdrawConfig.setEditTime(LocalDateTime.now()); + freeWithdrawConfig.setIsDelete(freeWithdraw.getIsDelete()); + freeWithdrawConfig.setRate(freeWithdraw.getRate()); + freeWithdrawConfigDao.updateById(freeWithdrawConfig); + } + } + + @Override + public FreeWithdrawConfig isFree(Long uid, LocalDateTime withdrawRequestTime) { + List freeWithdrawConfigList = freeWithdrawConfigDao.listOrderByEndTime(uid); + if (CollectionUtils.isEmpty(freeWithdrawConfigList)) { + return null; + } + FreeWithdrawConfig result = null; + for (FreeWithdrawConfig freeWithdrawConfig : freeWithdrawConfigList) { + if (freeWithdrawConfig.getEndTime() == null) { + if (withdrawRequestTime.isAfter(freeWithdrawConfig.getStartTime())) { + return freeWithdrawConfig; + } + } else { + if (withdrawRequestTime.isAfter(freeWithdrawConfig.getStartTime()) && withdrawRequestTime.isBefore(freeWithdrawConfig.getEndTime())) { + return freeWithdrawConfig; + } + } + } + return result; + } + + + /** + * 获取所有 免费的用户 ID + * + * @return + */ + @Override + public List getAllFreeUserIds() { + List userIds = Lists.newArrayList(); + List freeWithdrawConfigList = freeWithdrawConfigDao.selectList(Wrappers.lambdaQuery() + .eq(FreeWithdrawConfig::getIsDelete, Boolean.FALSE) + .eq(FreeWithdrawConfig::getReason, FreeWithdrawReason.ANGEL_USER)); + + userIds.addAll(freeWithdrawConfigList.stream().map(FreeWithdrawConfig::getUid).collect(Collectors.toList())); + + List freeWithdrawConfigList2 = freeWithdrawConfigDao.selectList(Wrappers.lambdaQuery() + .eq(FreeWithdrawConfig::getIsDelete, Boolean.FALSE) + .lt(FreeWithdrawConfig::getStartTime, LocalDateTime.now()) + .gt(FreeWithdrawConfig::getEndTime, LocalDateTime.now()) + .in(FreeWithdrawConfig::getReason, FreeWithdrawReason.SHOP_LEVEL, FreeWithdrawReason.OPERATION)); + + userIds.addAll(freeWithdrawConfigList2.stream().map(FreeWithdrawConfig::getUid).collect(Collectors.toList())); + return userIds; + } + + + @Override + public FreeWithdrawConfig getFreeReson(Long uid) { + List freeWithdrawConfigList = freeWithdrawConfigDao.listOrderByEndTime(uid); + if (CollectionUtils.isEmpty(freeWithdrawConfigList)) { + return null; + } + FreeWithdrawConfig result = null; + for (FreeWithdrawConfig freeWithdrawConfig : freeWithdrawConfigList) { + if (freeWithdrawConfig.getEndTime() == null) { + if (LocalDateTime.now().isAfter(freeWithdrawConfig.getStartTime())) { + return freeWithdrawConfig; + } + } else { + if (LocalDateTime.now().isAfter(freeWithdrawConfig.getStartTime()) && LocalDateTime.now().isBefore(freeWithdrawConfig.getEndTime())) { + return freeWithdrawConfig; + } + } + } + return null; + } + + @Override + public WithdrawFeeInput calculateWithdrawFee(PayTrade payTrade) { + WithdrawFeeInput withdrawFeeInput = new WithdrawFeeInput(); + //获取当天的 免手续费配置 + FreeWithdrawConfig freeWithdrawConfig = this.isFree(payTrade.getSrcAccountId(), payTrade.getCreateTime()); + //如果不为空 就免手续费 并且 新增免手续费记录表 + BigDecimal reduceRate = BigDecimal.ZERO; + if (freeWithdrawConfig != null) { + //减免比例 + reduceRate = freeWithdrawConfig.getRate(); + //如果减免原因是SHOP_LEVEL,则需要检查下开关是否开了,减免时间是否到期 + if (FreeWithdrawReason.SHOP_LEVEL.equals(freeWithdrawConfig.getReason())) { + boolean enableEstar2WithdrawFeeReduction = payConfigService.enableWithdrawFeeReduction(); + //开关没开,不减免 + if (!enableEstar2WithdrawFeeReduction) { + reduceRate = BigDecimal.ZERO; + } + //超过了配置的时间,不减免 + LocalDateTime estar2WithdrawFeeReductionEndTime = payConfigService.withdrawFeeReductionEndTime(); + if (estar2WithdrawFeeReductionEndTime != null && LocalDateTime.now().isAfter(estar2WithdrawFeeReductionEndTime)) { + reduceRate = BigDecimal.ZERO; + } + } + } + + if (reduceRate.compareTo(BigDecimal.ZERO) > 0) { + //说明减免,新增免手续费记录表 + FreeWithdrawFeeBill freeWithdrawFeeBill = FreeWithdrawFeeBill.builder() + .uid(payTrade.getSrcAccountId()) + .tradeNo(payTrade.getTradeNo()) + .freeConfigId(freeWithdrawConfig.getId()) + .reason(freeWithdrawConfig.getReason()) + .editTime(LocalDateTime.now()) + .deleted(Boolean.FALSE) + .createTime(LocalDateTime.now()).build(); + freeWithdrawFeeBillDao.insert(freeWithdrawFeeBill); + withdrawFeeInput.setFreeWithdrawBillId(freeWithdrawFeeBill.getId()); + } + + //V6.12.0 平台默认收取10% 根据减免配置,收取一定的平台费用,6.19.1 版本调整成 20% + BigDecimal platfromFeeRate = new BigDecimal(stripeWithdrawFeeRate.doubleValue()); + withdrawFeeInput.setPlatformFee(calcPlatformFee(platfromFeeRate.subtract(reduceRate), payTrade.getOccurAmount())); + withdrawFeeInput.setThirdFee(getThirdWithdrawFee(payTrade, true)); + withdrawFeeInput.setWithdrawFee(withdrawFeeInput.getPlatformFee() + withdrawFeeInput.getThirdFee()); + return withdrawFeeInput; + } + + /** + * 计算平台收取费用 + * + * @param rate + * @param occurAmount + * @return + */ + private Long calcPlatformFee(BigDecimal rate, Long occurAmount) { + //处理精度丢失的问题,只保留小数点后两位 (0.200000000000000011102230246251565404236316680908203125) + rate = rate.setScale(2, BigDecimal.ROUND_DOWN); + log.info("===> calcPlatformFee rate : {}, occurAmount : {}", rate, occurAmount); + return rate.multiply(new BigDecimal(occurAmount)).setScale(0, BigDecimal.ROUND_UP).longValue(); + } + + private Long getThirdWithdrawFee(PayTrade payTrade, Boolean free) { + if (payTrade.getPayChannel().equals(PayChannel.STRIPE)) { + //手续费默认为 25L; + return stripeWithdrawFeeBase; + } else { + ToastResultCode.UNSUPPORTED_BUSINESS_TYPE.check(true); + } + return 0L; + } + + + @Override + public WithdrawFeeReduceInfoOutput getWithdrawFeeReduceInfo(Long userId) { + WithdrawFeeReduceInfoOutput output = new WithdrawFeeReduceInfoOutput(); + FreeWithdrawConfig freeWithdrawConfig = getFreeReson(userId); + if (freeWithdrawConfig == null) { + output.setIsWithdrawFeeReduce(false); + return output; + } + //SHOP_LEVEL类型 + if (FreeWithdrawReason.SHOP_LEVEL.equals(freeWithdrawConfig.getReason())) { + Boolean isWithdrawFeeReduction = true; + boolean enableEstar2WithdrawFeeReduction = payConfigService.enableWithdrawFeeReduction(); + //开关没开,不减免 + if (!enableEstar2WithdrawFeeReduction) { + isWithdrawFeeReduction = false; + } + //超过了配置的时间,不减免 + LocalDateTime estar2WithdrawFeeReductionEndTime = payConfigService.withdrawFeeReductionEndTime(); + if (estar2WithdrawFeeReductionEndTime != null && LocalDateTime.now().isAfter(estar2WithdrawFeeReductionEndTime)) { + isWithdrawFeeReduction = false; + } + if (!isWithdrawFeeReduction) { + output.setIsWithdrawFeeReduce(false); + return output; + } + } + output.setIsWithdrawFeeReduce(true); + output.setFreeWithdrawReason(freeWithdrawConfig.getReason()); + output.setReduceRate(freeWithdrawConfig.getRate()); + String text = MessageUtils.get(freeWithdrawConfig.getReason().name()); + String rate = freeWithdrawConfig.getRate().multiply(BigDecimal.valueOf(100)).toString(); + output.setTip(String.format(text, rate + "%")); + return output; + } + +} + + + diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/GoogleRecordServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/GoogleRecordServiceImpl.java new file mode 100644 index 0000000..dad6c56 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/GoogleRecordServiceImpl.java @@ -0,0 +1,39 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.dao.GoogleRecordDao; +import com.sonic.lion.domain.entity.GoogleRecord; +import com.sonic.lion.service.GoogleRecordService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +/** + * @author: code + * @date: 2025/06/24 + * @Description: + * @version: 1.0.0 + */ +@Service +public class GoogleRecordServiceImpl implements GoogleRecordService { + + @Autowired + private GoogleRecordDao googleRecordDao; + + @Override + public void save(@NonNull GoogleRecord googleRecord) { + googleRecordDao.insert(googleRecord); + } + + @Override + public GoogleRecord getByTransactionId(String transactionId) { + if (StringUtils.isBlank(transactionId)) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(GoogleRecord::getTransactionId, transactionId); + return googleRecordDao.selectOne(queryWrapper); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/GoogleServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/GoogleServiceImpl.java new file mode 100644 index 0000000..7796801 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/GoogleServiceImpl.java @@ -0,0 +1,445 @@ +package com.sonic.lion.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.exception.SysException; +import com.sonic.common.rpc.GlobalResultCode; +import com.sonic.lion.enums.TradeStatus; +import com.sonic.lion.dao.UserSubscriptionNotifyDao; +import com.sonic.lion.domain.entity.*; +import com.sonic.lion.domain.enums.TradeEvent; +import com.sonic.lion.domain.req.GoogleUploadReceiptReq; +import com.sonic.lion.service.*; +import com.sonic.lion.enums.ToastResultCode; +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.SecurityUtils; +import com.google.api.services.androidpublisher.AndroidPublisher; +import com.google.api.services.androidpublisher.AndroidPublisherScopes; +import com.google.api.services.androidpublisher.model.ProductPurchase; +import com.google.api.services.androidpublisher.model.SubscriptionPurchase; +import com.google.api.services.androidpublisher.model.VoidedPurchase; +import com.google.api.services.androidpublisher.model.VoidedPurchasesListResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.math.BigDecimal; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * @author: code + * @date: 2025/06/22 + * @Description: 不把Google内购这个奇葩当作支付渠道 + * @version: 1.0.0 + */ +@Slf4j +@Service +public class GoogleServiceImpl implements GoogleService, InitializingBean { + + @Autowired + private PayTradeService payTradeService; + + @Autowired + private GoogleRecordService googleRecordService; + + @Autowired + private PayChargeService payChargeService; + + @Autowired + private PlatformTransactionManager transactionManager; + + @Autowired + private PayConfigService payConfigService; + + @Autowired + private GoogleUploadReceiptService googleUploadReceiptService; + + @Autowired + private UserSubscriptionService userSubscriptionService; + + @Value("${google.boundId}") + private String BUNDLE_ID; + + @Value("${google.serviceAccountId}") + private String serviceAccountId; + + @Value("${google.packageName}") + private String packageName; + + @Value("${site.type}") + private String siteType; + + private static final BigDecimal MINUS_ONE = new BigDecimal(-1); + + PrivateKey privateKey; + + @Autowired + private UserSubscriptionNotifyDao userSubscriptionNotifyDao; + + @PostConstruct + public void init() { + try { + ClassPathResource resource = new ClassPathResource(siteType + "/api-google.p12"); + privateKey = SecurityUtils.loadPrivateKeyFromKeyStore( + SecurityUtils.getPkcs12KeyStore(), + resource.getInputStream(), // 生成的P12文件 + "notasecret", "privatekey", "notasecret"); + } catch (Exception e) { + log.error("===> init google secret error : ", e); + throw new SysException(GlobalResultCode.SYSTEM_EXCEPTION, e); + } + } + + @Override + public Long calculatePaymentFee(@NonNull Long productAmount) { + //蹩脚设计 为了兼容之前不传productId 版本。明明可以直接从productId取得 + String googleFeeRatio = payConfigService.getGoogleFeeRatio(); + BigDecimal number1 = new BigDecimal(googleFeeRatio.split("/")[0]); + BigDecimal number2 = new BigDecimal(googleFeeRatio.split("/")[1]); + return new BigDecimal(productAmount).multiply(number1).divide(number2, 0, BigDecimal.ROUND_HALF_UP).add(MINUS_ONE).longValue(); + } + + @Override + public Long calculatePaymentFee(PayCharge payCharge) { + return payCharge.getPayAmount() - payCharge.getChargeAmount(); + } + + @Override + public void uploadReceipt(@NonNull GoogleUploadReceiptReq req) throws GeneralSecurityException, IOException { + PayTrade payTrade = payTradeService.getByTradeNo(req.getTradeNo()); + if(TradeStatus.FINISHED.equals(payTrade.getStatus())){ + return ; + } + //先保存票据 + Map transactions = new HashMap<>(); + transactions.put(req.getTradeNo(), req.getReceipt()); + GoogleUploadReceipt googleUploadReceipt = GoogleUploadReceipt.builder() + .transactionsJsonStr(JSONObject.toJSONString(transactions)) + .receipt(req.getReceipt()) + .processed(Boolean.FALSE) + .build(); + try { + googleUploadReceiptService.save(googleUploadReceipt); + } catch (DuplicateKeyException e) { + log.warn("该google收据已保存"); + return; + } + + req.setReceiptId(googleUploadReceipt.getId()); + boolean processed = processReceipt(req); + //处理完成后更新处理状态 + googleUploadReceiptService.updateProcessed(googleUploadReceipt.getId(), processed); + } + + @Override + public void processReceipt() { +// LocalDateTime startCreateTime = LocalDateTime.now().minusHours(24L); +// List list = googleUploadReceiptService.list(Boolean.FALSE, startCreateTime, 10); +// for (GoogleUploadReceipt googleUploadReceipt : list) { +// try { +// log.info("google 收据补偿处理, receiptId: {}", googleUploadReceipt.getId()); +// JSONObject transactions = JSON.parseObject(googleUploadReceipt.getTransactionsJsonStr()); +// GoogleUploadReceiptReq req = GoogleUploadReceiptReq.builder() +// .receiptId(googleUploadReceipt.getId()) +// .transactions(transactions) +// .receipt(googleUploadReceipt.getReceipt()) +// .build(); +// +// boolean processed = processReceipt(req); +// //处理完成后更新处理状态 +// googleUploadReceiptService.updateProcessed(googleUploadReceipt.getId(), processed); +// } catch (Exception e) { +// log.error("google收据处理失败, receiptId: {}", googleUploadReceipt.getId()); +// } +// } + } + + /** + * 处理收据 + * @param req + * @return 是否处理完成 + */ + private boolean processReceipt(GoogleUploadReceiptReq req) throws GeneralSecurityException, IOException { + String tradeNo = req.getTradeNo(); + //找不到相应的tradeNo + if (StringUtils.isBlank(tradeNo)) { + return false; + } + ProductPurchase productPurchase = googleCheck(req.getProductId(), req.getReceipt()); + req.setTransactionId(productPurchase.getOrderId()); + //判断状态 + if (productPurchase.getConsumptionState() != 1 && productPurchase.getPurchaseState() != 0) { + ToastResultCode.GOOGLE_TICKET_STATUS_ERROR.check(true); + return false; + } + PayCharge payCharge = payChargeService.getByBundleIdAndProductIdAndPlatform(BUNDLE_ID, req.getProductId(), "android"); + PayTrade payTrade = payTradeService.getByTradeNo(tradeNo); + if (!Objects.equals(payCharge.getPayAmount(), payTrade.getOccurAmount())) { + log.error("付款金额不匹配"); + return false; + } + GoogleRecord iapRecord = googleRecordService.getByTransactionId(req.getTransactionId()); + if (iapRecord == null) { + //事务 + TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition()); + try { + GoogleRecord googleRecord = GoogleRecord.builder() + .tradeNo(tradeNo) + .transactionId(req.getTransactionId()) + .receiptId(req.getReceiptId()) + .bundleId(BUNDLE_ID) + .productId(req.getProductId()) + .result(JSONObject.toJSONString(productPurchase)) + .build(); + googleRecordService.save(googleRecord); + + //未处理的收据,可开始交易处理 + payTradeService.tradeHandle(payTrade, TradeEvent.PAYMENT); + transactionManager.commit(transactionStatus); + } catch (Exception e) { + transactionManager.rollback(transactionStatus); + log.error("google receipt process error", e); + } + } + return true; + } + + /** + * 生产环境沙盒充值逻辑, 提审账号校验 + * + * @param accountId + */ + private void sandBoxChargeCheck(Long accountId) { +// List reviewAccountList = payConfigService.listReviewAccount(); +// SysExceptionUtils.check(reviewAccountList == null || !reviewAccountList.contains(accountId), "", "生产环境校验沙盒环境收据,但交易发起账户不是iOS提审账户"); +// //提审账号最大可充值金额校验 +// BizExceptionUtils.check(payTradeService.sumChargeOccurAmount(accountId) > AccountConstant.REVIEW_ACCOUNT_MAX_CHARGE_AMOUNT, "", "The top-up amount on the day cannot exceed $1000.00"); + } + + @Override + public ProductPurchase googleCheck(String productId, String token) throws GeneralSecurityException, IOException { +// Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 10001)); +// HttpTransport httpTransport = new NetHttpTransport.Builder().setProxy(proxy).build(); + + HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + GoogleCredential credential = new GoogleCredential.Builder() + .setTransport(httpTransport).setJsonFactory(JacksonFactory.getDefaultInstance()) + .setServiceAccountId(serviceAccountId) + .setServiceAccountScopes(AndroidPublisherScopes.all()) + .setServiceAccountPrivateKey(privateKey).build(); + + AndroidPublisher publisher = new AndroidPublisher.Builder(httpTransport, + JacksonFactory.getDefaultInstance(), credential).build(); + + AndroidPublisher.Purchases.Products products = publisher.purchases().products(); + AndroidPublisher.Purchases.Products.Get product = products.get(packageName, productId, token); + ProductPurchase purchase = product.execute(); + return purchase; + } + + + @Override + public SubscriptionPurchase getSubscriptionPurchase(String productId, String token) throws GeneralSecurityException, IOException { + HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + GoogleCredential credential = new GoogleCredential.Builder() + .setTransport(httpTransport).setJsonFactory(JacksonFactory.getDefaultInstance()) + .setServiceAccountId(serviceAccountId) + .setServiceAccountScopes(AndroidPublisherScopes.all()) + .setServiceAccountPrivateKey(privateKey).build(); + + AndroidPublisher publisher = new AndroidPublisher.Builder(httpTransport, + JacksonFactory.getDefaultInstance(), credential).build(); + + AndroidPublisher.Purchases.Subscriptions sub = publisher.purchases().subscriptions(); + AndroidPublisher.Purchases.Subscriptions.Get get = sub.get(packageName, productId, token); + SubscriptionPurchase subscriptionPurchase = get.execute(); + return subscriptionPurchase; + } + + + @Override + public void handReceiptSubscribe(String productId, Long receiptId, Long userId) { + //productId 变了就是升级了。 + try { + GoogleUploadReceipt googleUploadReceipt = googleUploadReceiptService.getById(receiptId) ; + String receipt = googleUploadReceipt.getReceipt(); + SubscriptionPurchase subscriptionPurchase = getSubscriptionPurchase(productId, receipt); + Long expiresDateMs = Long.valueOf(subscriptionPurchase.getExpiryTimeMillis()); + LocalDateTime expTime = LocalDateTime.ofEpochSecond(expiresDateMs/1000, 0, ZoneOffset.ofHours(8)); + String subscriptionId = subscriptionPurchase.getOrderId().substring(0, 24); + userSubscriptionService.bind(subscriptionId, UserSubscription.Platform.GOOGLE,expTime,LocalDateTime.now() ,productId,userId,subscriptionPurchase.getLinkedPurchaseToken(),null); + googleUploadReceiptService.updateProcessed(receiptId,true); + }catch (Exception e){ + log.info("处理上传Google 订阅 凭据失败:{},{}", productId ,receiptId); + log.error("处理上传Google 订阅 凭据失败",e); + } + } + + + + @Override + public List voidedPurchases(LocalDateTime startTime) throws GeneralSecurityException, IOException { + HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + GoogleCredential credential = new GoogleCredential.Builder() + .setTransport(httpTransport).setJsonFactory(JacksonFactory.getDefaultInstance()) + .setServiceAccountId(serviceAccountId) + .setServiceAccountScopes(AndroidPublisherScopes.all()) + .setServiceAccountPrivateKey(privateKey).build(); + AndroidPublisher publisher = new AndroidPublisher.Builder(httpTransport, + JacksonFactory.getDefaultInstance(), credential).build(); + //获取google list对象 + AndroidPublisher.Purchases.Voidedpurchases.List voidPurchaseList = publisher.purchases().voidedpurchases().list(packageName); + Long milliseconds = startTime.atZone(ZoneOffset.UTC).toInstant().toEpochMilli(); + //设置查询参数 +// voidPurchaseList.setStartTime(milliseconds); + //执行查询 + log.info("开始查询google退款:{}",milliseconds); + VoidedPurchasesListResponse response = voidPurchaseList.execute(); + List voidedPurchases = response.getVoidedPurchases(); + log.info("google退款结果:{}", voidedPurchases); + return voidedPurchases; + /** + * + * ZG444444444444RT5 购买商品名称 购买数量 购买金额 购买时间 退款时间 用户ID epal订单号 客户端(GP/苹果/web) + * [{"kind":"androidpublisher#voidedPurchase","purchaseTimeMillis":"1635348172211", + * "purchaseToken":"doibnmclnepnigcblgnodkgo.AO-J1OyK0N8BkSeDyyMpQeOG-Z7jcadCTMxkINqXVPZWcpl91YOqVkaOkWS4OqGl3WHk1XAXwx6P8DNQwMxLrmxXLGnM0ULr7g", + * "voidedTimeMillis":"1636432172312","orderId":"GPA.3327-1510-4846-05509","voidedSource":0,"voidedReason":7}] + */ + } + + /** + * + * https://developer.android.com/google/play/billing/subscriptions + * + * 订阅的 notificationType 可以具有以下值: + * (1) SUBSCRIPTION_RECOVERED - 从帐号保留状态恢复了订阅。 + * (2) SUBSCRIPTION_RENEWED - 续订了处于活动状态的订阅。 + * (3) SUBSCRIPTION_CANCELED - 自愿或非自愿地取消了订阅。如果是自愿取消,在用户取消时发送。 + * (4) SUBSCRIPTION_PURCHASED - 购买了新的订阅。 + * (5) SUBSCRIPTION_ON_HOLD - 订阅已进入帐号保留状态(如果已启用)。 + * (6) SUBSCRIPTION_IN_GRACE_PERIOD - 订阅已进入宽限期(如果已启用)。 + * (7) SUBSCRIPTION_RESTARTED - 用户已通过 Play > 帐号 > 订阅重新激活其订阅(需要选择使用订阅恢复功能)。 + * (8) SUBSCRIPTION_PRICE_CHANGE_CONFIRMED - 用户已成功确认订阅价格变动。 + * (9) SUBSCRIPTION_DEFERRED - 订阅的续订时间点已延期。 + * (10) SUBSCRIPTION_PAUSED - 订阅已暂停。 + * (11) SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED - 订阅暂停计划已更改。 + * (12) SUBSCRIPTION_REVOKED - 用户在到期时间之前已撤消订阅。 + * (13) SUBSCRIPTION_EXPIRED - 订阅已到期。 + * @param notifyId + * @param playload + * @return + * @throws Exception + */ + @Override + public boolean handSubscribeWebhook(Long notifyId, String type, String playload) throws Exception{ + doWhenStatusChange(notifyId,type, playload); + doWhenPay(notifyId,type, playload); + return false; + } + + /** + * 处理 google 状态变更。 + * @param nofityId + * @param type + * @param playload + * @throws GeneralSecurityException + * @throws IOException + */ + private void doWhenStatusChange(Long nofityId,String type,String playload) throws GeneralSecurityException, IOException { + String content = playload; + JSONObject jsonObject = JSON.parseObject(content); + JSONObject subscriptionNotification = jsonObject.getJSONObject("subscriptionNotification"); + String purchaseToken = subscriptionNotification.getString("purchaseToken"); + String productId = subscriptionNotification.getString("subscriptionId"); + + //根据状态来判断是否续期 + boolean autoRenewing ="SUBSCRIPTION_RESTARTED".equals(type)|| "SUBSCRIPTION_RECOVERED".equals(type) + || "SUBSCRIPTION_PURCHASED".equals(type) ||"SUBSCRIPTION_PRICE_CHANGE_CONFIRMED".equals(type) + ||"SUBSCRIPTION_DEFERRED".equals(type) || "SUBSCRIPTION_RENEWED".equals(type); + + SubscriptionPurchase subscriptionPurchase = this.getSubscriptionPurchase(productId, purchaseToken); + log.info("doWhenStatusChange nofityId:{}, subscriptionPurchase对象:{}" ,nofityId , subscriptionPurchase); + String subscriptionId = subscriptionPurchase.getOrderId().substring(0, 24); + + //暂停状态变更的时候 取 接口的。 + if("SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED".equals(type)){ + autoRenewing = subscriptionPurchase.getAutoRenewing(); + } + + userSubscriptionService.updateStatusByThird(subscriptionId, UserSubscription.Platform.GOOGLE , productId,autoRenewing); + + if (!autoRenewing) { + userSubscriptionService.cancel(subscriptionId, UserSubscription.Platform.GOOGLE, null); + } +// subscribeService.completeNotify(nofityId); + } + + private void doWhenPay(Long nofityId,String type,String playload) throws GeneralSecurityException, IOException { + + if(!"SUBSCRIPTION_RENEWED".equals(type) && !"SUBSCRIPTION_CANCELED".equals(type) && !"SUBSCRIPTION_ON_HOLD".equals(type)) { + return; + } + + String content = playload; + JSONObject jsonObject = JSON.parseObject(content); + + JSONObject subscriptionNotification = jsonObject.getJSONObject("subscriptionNotification"); + String purchaseToken = subscriptionNotification.getString("purchaseToken"); + String productId = subscriptionNotification.getString("subscriptionId"); + SubscriptionPurchase subscriptionPurchase = this.getSubscriptionPurchase(productId, purchaseToken); + + log.info("nofityId:{}, subscriptionPurchase对象:{}" ,nofityId , subscriptionPurchase); + + + String subscriptionId = subscriptionPurchase.getOrderId().substring(0, 24); + UserSubscription userSubscription = userSubscriptionService.getBySubscriptionId(subscriptionId); + ToastResultCode.DATA_NOT_EXITS.check(userSubscription == null); + userSubscriptionNotifyDao.setSubscriptionId(nofityId,subscriptionId); + LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(subscriptionPurchase.getExpiryTimeMillis()/1000, 0, ZoneOffset.ofHours(8)); + userSubscriptionService.setPurchaseToken(userSubscription.getId() , purchaseToken); + //处理续订 + if("SUBSCRIPTION_RENEWED".equals(type)) { + //变更权益 + if(subscriptionPurchase.getPaymentState() ==1 ){ + userSubscriptionService.bind(subscriptionId, UserSubscription.Platform.GOOGLE,localDateTime, LocalDateTime.now(),productId,null,purchaseToken,null); + } + //处理取消 + }else if("SUBSCRIPTION_CANCELED".equals(type)){ + if(Boolean.FALSE.equals(subscriptionPurchase.getAutoRenewing())){ + userSubscriptionService.cancel(userSubscription.getSubscriptionId(), UserSubscription.Platform.GOOGLE,subscriptionPurchase.getExpiryTimeMillis()); + } + //处理扣款失败 + }else if("SUBSCRIPTION_ON_HOLD".equals(type)){ + if(Boolean.FALSE.equals(subscriptionPurchase.getAutoRenewing())){ + userSubscriptionService.payFailSystemMessage(userSubscription.getSubscriptionId(), UserSubscription.Platform.GOOGLE); + } + } + userSubscriptionService.completeNotify(nofityId); + } + + @Override + public void afterPropertiesSet() { + + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/GoogleUploadReceiptServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/GoogleUploadReceiptServiceImpl.java new file mode 100644 index 0000000..f835840 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/GoogleUploadReceiptServiceImpl.java @@ -0,0 +1,55 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.dao.GoogleUploadReceiptDao; +import com.sonic.lion.domain.entity.GoogleUploadReceipt; +import com.sonic.lion.service.GoogleUploadReceiptService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author: code + * @date: 2025/07/15 + * @Description: + * @version: 1.0.0 + */ +@Service +public class GoogleUploadReceiptServiceImpl implements GoogleUploadReceiptService { + + @Autowired + private GoogleUploadReceiptDao googleUploadReceiptDao; + + @Override + public Long save(@NonNull GoogleUploadReceipt googleUploadReceipt) { + googleUploadReceiptDao.insert(googleUploadReceipt); + return googleUploadReceipt.getId(); + } + + @Override + public GoogleUploadReceipt getById(Long id) { + return googleUploadReceiptDao.selectById(id); + } + + @Override + public boolean countRecepit(String receipt) { + return googleUploadReceiptDao.selectCount(Wrappers.lambdaQuery().eq(GoogleUploadReceipt::getReceipt, receipt)) > 0; + } + + @Override + public int updateProcessed(@NonNull Long id, @NonNull Boolean processed) { + GoogleUploadReceipt updater = GoogleUploadReceipt.builder() + .id(id) + .processed(processed) + .build(); + return googleUploadReceiptDao.updateById(updater); + } + + @Override + public List list(Boolean processed, LocalDateTime startCreateTime, Integer num) { + return googleUploadReceiptDao.list(processed, startCreateTime, num); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapRecordServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapRecordServiceImpl.java new file mode 100644 index 0000000..dbf21b5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapRecordServiceImpl.java @@ -0,0 +1,39 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.dao.IapRecordDao; +import com.sonic.lion.domain.entity.IapRecord; +import com.sonic.lion.service.IapRecordService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +/** + * @author: code + * @date: 2025/06/24 + * @Description: + * @version: 1.0.0 + */ +@Service +public class IapRecordServiceImpl implements IapRecordService { + + @Autowired + private IapRecordDao iapRecordDao; + + @Override + public void save(@NonNull IapRecord iapRecord) { + iapRecordDao.insert(iapRecord); + } + + @Override + public IapRecord getByTransactionId(String transactionId) { + if (StringUtils.isBlank(transactionId)) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(IapRecord::getTransactionId, transactionId); + return iapRecordDao.selectOne(queryWrapper); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapServiceImpl.java new file mode 100644 index 0000000..688cc2f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapServiceImpl.java @@ -0,0 +1,714 @@ +package com.sonic.lion.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.dao.*; +import com.sonic.lion.domain.entity.*; +import com.sonic.lion.domain.enums.TradeEvent; +import com.sonic.lion.domain.input.IOSReceiptInput; +import com.sonic.lion.domain.req.IapUploadReceiptReq; +import com.sonic.lion.service.*; +import com.sonic.lion.utils.IOSConsumptionUtil; +import com.sonic.lion.utils.NetRetryUtils; +import com.sonic.lion.enums.ToastResultCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Objects; + +/** + * @author: code + * @date: 2025/06/22 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@Service("iapService") +public class IapServiceImpl implements IapService, InitializingBean { + + @Autowired + private PayTradeService payTradeService; + + @Autowired + private PayTradeDao payTradeDao; + + @Autowired + private IapRecordService iapRecordService; + + @Autowired + private IapUploadReceiptService iapUploadReceiptService; + + @Autowired + private PayChargeService payChargeService; + + @Autowired + private PlatformTransactionManager transactionManager; + + @Autowired + private PayConfigService payConfigService; + + @Value("${spring.profiles.active}") + private String active; + + @Autowired + private UserSubscriptionService userSubscriptionService; + + @Autowired + private UserSubscriptionDao userSubscriptionDao; + + @Autowired + private NetRetryUtils netRetryUtils; + + @Autowired + private AccountBuffBillDao accountBuffBillDao; + + @Autowired + private AccountBuffService accountBuffService; + + @Autowired + private IOSConsumptionUtil iosConsumptionUtil; + + private static final int SUCC_STATUS = 0; + + private static final int SANDBOX_RECIPT = 21007; + + private static final String PRODUCT_VERIFY_RECEIPT_URL = "https://buy.itunes.apple.com/verifyReceipt"; + + private static final String SANDBOX_VERIFY_RECEIPT_URL = "https://sandbox.itunes.apple.com/verifyReceipt"; + + private static final BigDecimal MINUS_ONE = new BigDecimal(-1); + + @Value("${apple.boundId}") + private String BUNDLE_ID; + + @Value("${apple.password}") + private String password; + + @Value("${apple.verifyReceiptUrl}") + private String verifyReceiptUrl; + + @Autowired + private AppleRefundRecordDao appleRefundRecordDao; + + @Autowired + private UserSubscriptionNotifyDao userSubscriptionNotifyDao; + + @Autowired + private ChannelBlacklistService channelBlacklistService; + + @Override + public Long calculatePaymentFee(@NonNull Long productAmount) { + //蹩脚设计 为了兼容之前不传productId 版本。明明可以直接从productId取得 + String iosFeeRatio = payConfigService.getIOSFeeRatio(); + BigDecimal number1 = new BigDecimal(iosFeeRatio.split("/")[0]); + BigDecimal number2 = new BigDecimal(iosFeeRatio.split("/")[1]); + return new BigDecimal(productAmount).multiply(number1).divide(number2, 0, BigDecimal.ROUND_HALF_UP).add(MINUS_ONE).longValue(); + } + + + @Override + public void uploadReceipt(@NonNull IapUploadReceiptReq req, Long currentUserId) { + JSONObject jsonObject = req.getTransactions(); + String tradeNo = null; + for (String key : jsonObject.keySet()) { + tradeNo = jsonObject.getString(key); + break; + } + //先保存票据 + IapUploadReceipt iapUploadReceipt = IapUploadReceipt.builder() + .transactionsJsonStr(req.getTransactions().toJSONString()) + .receipt(req.getReceipt()) + .processed(Boolean.FALSE) + .userId(currentUserId) + .tradeNo(tradeNo) + .build(); + try { + iapUploadReceiptService.save(iapUploadReceipt); + } catch (DuplicateKeyException e) { + log.warn("该iap收据已保存"); + return; + } + + req.setReceiptId(iapUploadReceipt.getId()); + boolean processed = processReceipt(req); + //处理完成后更新处理状态 + iapUploadReceiptService.updateProcessed(iapUploadReceipt.getId(), processed); + } + + @Override + public void processReceipt() { + LocalDateTime startCreateTime = LocalDateTime.now().minusHours(24L); + List list = iapUploadReceiptService.list(Boolean.FALSE, startCreateTime, 20); + for (IapUploadReceipt iapUploadReceipt : list) { + if (iapUploadReceipt.getUserId() != null) { + log.info("iap收据补偿订阅, receiptId: {}", iapUploadReceipt.getId()); + handReceiptSubscribe(null, iapUploadReceipt.getId(), iapUploadReceipt.getUserId()); + } else { + try { + log.info("iap 收据补偿处理, receiptId: {}", iapUploadReceipt.getId()); + JSONObject transactions = JSON.parseObject(iapUploadReceipt.getTransactionsJsonStr()); + IapUploadReceiptReq req = IapUploadReceiptReq.builder() + .receiptId(iapUploadReceipt.getId()) + .transactions(transactions) + .receipt(iapUploadReceipt.getReceipt()) + .build(); + + boolean processed = processReceipt(req); + //处理完成后更新处理状态 + iapUploadReceiptService.updateProcessed(iapUploadReceipt.getId(), processed); + } catch (Exception e) { + log.error("iap收据处理失败, receiptId: {}", iapUploadReceipt.getId()); + } + } + } + } + + @Override + public void afterPropertiesSet() { + if ("product".equals(active)) { + verifyReceiptUrl = PRODUCT_VERIFY_RECEIPT_URL; + } else { + verifyReceiptUrl = SANDBOX_VERIFY_RECEIPT_URL; + } + } + + + @Override + public boolean handReceiptSubscribe(String productId, Long receiptId, Long userId) { + String receipt = iapUploadReceiptService.getById(receiptId).getReceipt(); + JSONObject latestReceiptInfo = checkAndGeLatestReceiptInfo(receipt); + log.info("handReceiptSubscribe latestReceiptInfo{}", latestReceiptInfo.toJSONString()); + /** + * "quantity": "1", + * "product_id": "com.sonic.autorenew_0001", + * "transaction_id": "1000000863749566", + * "original_transaction_id": "1000000860235555", + * "purchase_date": "2021-08-21 08:41:34 Etc/GMT", + * "purchase_date_ms": "1629535294000", + * "purchase_date_pst": "2021-08-21 01:41:34 America/Los_Angeles", + * "original_purchase_date": "2021-08-16 13:05:35 Etc/GMT", + * "original_purchase_date_ms": "1629119135000", + * "original_purchase_date_pst": "2021-08-16 06:05:35 America/Los_Angeles", + * "expires_date": "2021-08-21 08:46:34 Etc/GMT", + * "expires_date_ms": "1629535594000", + * "expires_date_pst": "2021-08-21 01:46:34 America/Los_Angeles", + * "web_order_line_item_id": "1000000065173763", + * "is_trial_period": "false", + * "is_in_intro_offer_period": "false", + * "in_app_ownership_type": "PURCHASED", + * "subscription_group_identifier": "20867190" + */ + String originalTransactionId = latestReceiptInfo.getString("original_transaction_id"); + String transactionId = latestReceiptInfo.getString("transaction_id"); + String product_id = latestReceiptInfo.getString("product_id"); + //处理兼容前端没有传商品ID的情况 + if(StringUtils.isEmpty(productId)) { + log.info("===> handReceiptSubscribe apple org productId : {}, after productId : {}", productId, product_id); + productId = product_id; + } + + //过期时间 + Long expiresDateMs = Long.valueOf(latestReceiptInfo.getString("expires_date_ms")); + //订购时间 + Long purchase_date_ms = Long.valueOf(latestReceiptInfo.getString("purchase_date_ms")); + + IOSReceiptInput iosReceiptInput = IOSReceiptInput.builder().originalTransactionId(originalTransactionId).transactionId(transactionId) + .productId(product_id).expires_date_ms(expiresDateMs).purchase_date_ms(purchase_date_ms).build(); + + userSubscriptionService.bind(iosReceiptInput.getSubscriptionId(), UserSubscription.Platform.APPLE, iosReceiptInput.getExpiresDate(), iosReceiptInput.getPurchaseDate(), productId, userId, null, null); + iapUploadReceiptService.updateProcessed(receiptId, true); + return true; + } + + + /** + * https://developer.apple.com/documentation/appstoreservernotifications/notification_type 相关链接. + * + * @param receiptId + * @param playload + * @return + */ + @Override + public boolean handSubscribeWebhook(Long receiptId, String playload) { + //将处理订阅的异常给吃掉,不能影响后面的业务逻辑的执行 + try { + doWhenStatusChange(receiptId, playload); + } catch (Exception e) { + log.error("处理IOS回调失败notifyId2:"+ receiptId, e); + } + doWhenCancel(receiptId, playload); + doWhenPay(receiptId, playload); + doWhenPayFail(receiptId, playload); + //处理收到退款回调 + doWhenRefundV2(receiptId, playload); + doWhenConsumptionRequest(receiptId, playload); + return true; + } + + + //当状态变更的时候 更新套餐 和 续订状态。 + @Transactional(rollbackFor = Exception.class) + @Override + public void doWhenStatusChange(Long receiptId, String playload) { + JSONObject result = JSON.parseObject(playload); + String original_transaction_id = result.getString("original_transaction_id"); + if (original_transaction_id == null) { + return; + } + String productId = result.getString("auto_renew_product_id"); + Boolean autoRenew = !"false".equals(result.getString("auto_renew_status")); + userSubscriptionService.updateStatusByThird(original_transaction_id, UserSubscription.Platform.APPLE , productId,autoRenew); + if (!autoRenew) { + userSubscriptionService.cancel(original_transaction_id, UserSubscription.Platform.APPLE, null); + } + userSubscriptionService.completeNotify(receiptId); + } + + + @Transactional(rollbackFor = Exception.class) + @Override + public void doWhenConsumptionRequest(Long receiptId, String playload) { + JSONObject result = JSON.parseObject(playload); + String notification_type = result.getString("notification_type"); + if (!"CONSUMPTION_REQUEST".equals(notification_type)) { + return; + } + //校验是IOS服务器 + if (!result.getString("password").equals(password)) { + return; + } + String original_transaction_id = result.getString("original_transaction_id"); + IapRecord record = iapRecordService.getByTransactionId(original_transaction_id); + if (record == null) { + log.info("===> doWhenConsumptionRequest IapRecord not found , receiptId={}", receiptId); + return; + } + int count = appleRefundRecordDao.selectCount(Wrappers.lambdaQuery() + .eq(AppleRefundRecord::getTransactionId, original_transaction_id)); + //已经存在的数据 不再处理 + if (count > 0) { + log.info("===> doWhenConsumptionRequest IapRecord data found , receiptId={}", receiptId); + return; + } + String tradeNo = record.getTradeNo(); + PayTrade payTrade = payTradeService.getByTradeNo(tradeNo); + + int consumptionStatus = 0; + //购买得到的buff + Long getBuffAmount = payTrade.getAmount(); + //这期间 消费了的buff + Long sumConsumptionTotal = accountBuffBillDao.sumConsumption(payTrade.getPayTime(), LocalDateTime.now(), payTrade.getSrcAccountId()); + if (sumConsumptionTotal == 0) { //没有消费 扣除buff 并发送系统消息。 + consumptionStatus = 1; //不消耗应用内购买 + } else if (sumConsumptionTotal > 0 && sumConsumptionTotal < getBuffAmount) { + consumptionStatus = 2; //应用内购买被部分消费 + } else { + consumptionStatus = 3; //应用内购买已完全消耗 + } + + + /** + * {"transaction_id":"200001174368839","original_purchase_date":"2021-12-25 15:42:33 Etc/GMT", + * "in_app_ownership_type":"PURCHASED","quantity":"1","original_transaction_id":"200001174368839", + * "purchase_date_pst":"2021-12-25 07:42:33 America/Los_Angeles","original_purchase_date_ms": + * "1640446953000","purchase_date_ms":"1640446953000","product_id":"gg.epal.buff_0004", + * "original_purchase_date_pst":"2021-12-25 07:42:33 America/Los_Angeles", + * "is_trial_period":"false","purchase_date":"2021-12-25 15:42:33 Etc/GMT"} + */ + + JSONObject latestReceiptInfo = JSONObject.parseObject(record.getResult()); + //订购时间 + Long purchase_date_ms = Long.valueOf(latestReceiptInfo.getString("purchase_date_ms")); + LocalDateTime purchaseDate = LocalDateTime.ofEpochSecond(purchase_date_ms / 1000, 0, ZoneOffset.ofHours(8)); + + + Integer totalBuy = payTradeDao.countTotalBuy(payTrade.getSrcAccountId()); + Integer totalRefund = payTradeDao.countTotalRefund(payTrade.getSrcAccountId()); + //发送IOS 消费. + String json = iosConsumptionUtil.sendConsumption(original_transaction_id, 1, consumptionStatus, totalBuy, totalRefund, 1); + +// accountBuffService.fronzenBalanceAndSendMessage(user.getId(), getBuffAmount); //冻结 金额并 发送消息 + + //开始处理 冻结 + AppleRefundRecord.FronzenStatus fronzenStatus = accountBuffService.fronzenBalanceAndSendMessage(payTrade.getSrcAccountId(), payTrade.getAmount()); + + AppleRefundRecord appleRefundRecord = AppleRefundRecord.builder().productId(latestReceiptInfo.getString("product_id")) + .platform(AppleRefundRecord.Platform.IOS).bizType(BizType.CHARGE) + .content(playload) + .transactionId(original_transaction_id).cancellationDate(LocalDateTime.now()).purchaseDate(purchaseDate).build(); + + appleRefundRecord.setAmount(payTrade.getOccurAmount()); + appleRefundRecord.setUserId(payTrade.getSrcAccountId()); + appleRefundRecord.setTradeNo(tradeNo); + appleRefundRecord.setBuff(payTrade.getAmount()); + + appleRefundRecord.setFronzenStatus(fronzenStatus); + AccountBuff accountBuff = accountBuffService.getByUid(payTrade.getSrcAccountId()); + appleRefundRecord.setAfterFronzenBuff(accountBuff.getBalance()); + appleRefundRecord.setInsertTime(LocalDateTime.now()); + appleRefundRecordDao.insert(appleRefundRecord); + //将用户加入渠道黑名单中 + channelBlacklistService.addBlacklist(appleRefundRecord.getUserId(), PayChannel.STRIPE.name()); + + userSubscriptionService.updateNotifyExtend(receiptId, appleRefundRecord.getId(), json); + userSubscriptionService.completeNotify(receiptId); + } + + + private void doWhenRefund(Long receiptId, String playload) { + JSONObject result = JSON.parseObject(playload); + String notification_type = result.getString("notification_type"); + if (!"REFUND".equals(notification_type)) { + return; + } + //校验是IOS服务器 + if (!result.getString("password").equals(password)) { + return; + } + + JSONObject latestReceiptInfo = result.getJSONObject("unified_receipt").getJSONArray("latest_receipt_info").getJSONObject(0); + + + Long cancellation_date_ms = Long.valueOf(latestReceiptInfo.getString("cancellation_date_ms")); + LocalDateTime cancellation_date = LocalDateTime.ofEpochSecond(cancellation_date_ms / 1000, 0, ZoneOffset.ofHours(8)); + + + Long purchase_date_ms = Long.valueOf(latestReceiptInfo.getString("purchase_date_ms")); + LocalDateTime purchase_date = LocalDateTime.ofEpochSecond(purchase_date_ms / 1000, 0, ZoneOffset.ofHours(8)); + + String transaction_id = latestReceiptInfo.getString("transaction_id"); + AppleRefundRecord appleRefundRecord = AppleRefundRecord.builder().productId(latestReceiptInfo.getString("product_id")) + .platform(AppleRefundRecord.Platform.IOS).bizType(BizType.CHARGE) + .content(playload) + .transactionId(transaction_id).cancellationDate(cancellation_date).purchaseDate(purchase_date).build(); + + + IapRecord record = iapRecordService.getByTransactionId(transaction_id); + if (record != null) { + String tradeNo = record.getTradeNo(); + PayTrade payTrade = payTradeService.getByTradeNo(tradeNo); + appleRefundRecord.setAmount(payTrade.getOccurAmount()); + appleRefundRecord.setUserId(payTrade.getSrcAccountId()); + appleRefundRecord.setTradeNo(tradeNo); + appleRefundRecord.setBuff(payTrade.getAmount()); + } + appleRefundRecord.setInsertTime(LocalDateTime.now()); + appleRefundRecordDao.insert(appleRefundRecord); + //将用户加入渠道黑名单中 + channelBlacklistService.addBlacklist(appleRefundRecord.getUserId(), PayChannel.STRIPE.name()); + userSubscriptionService.completeNotify(receiptId); + } + + /** + * 处理收到渠道方的退款回调数据 + * @param payload + */ + private void doWhenRefundV2(Long receiptId, String payload) { + try { + JSONObject result = JSON.parseObject(payload); + String notification_type = result.getString("notification_type"); + if (!"REFUND".equals(notification_type)) { + return; + } + //校验是IOS服务器 + if (!result.getString("password").equals(password)) { + return; + } + //获取退款对应的原始交易号 + String transaction_id = result.getString("original_transaction_id"); + //根据交易号查询出对应的退款数据 + AppleRefundRecord appleRefundRecord = appleRefundRecordDao.selectOne(Wrappers.lambdaQuery() + .select(AppleRefundRecord::getId) + .eq(AppleRefundRecord::getTransactionId, transaction_id)); + if(appleRefundRecord == null) { + log.info("===> doWhenRefundV2 data not found"); + return; + } + //将请求的数据给更新过来 + userSubscriptionService.updateNotifyExtend(receiptId, appleRefundRecord.getId(), null); + } catch (Exception e) { + log.info("===> doWhenRefundV2 error"); + e.printStackTrace(); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void doWhenPayFail(Long receiptId, String playlod) { + JSONObject result = JSON.parseObject(playlod); + String notification_type = result.getString("notification_type"); + if (!"DID_FAIL_TO_RENEW".equals(notification_type)) { + return; + } + String latest_receipt = result.getJSONObject("unified_receipt").getString("latest_receipt"); + String env = result.getString("environment"); + JSONObject latestReceiptInfo = checkAndGeLatestReceiptInfoByEnv(latest_receipt, env); + + String originalTransactionId = latestReceiptInfo.getString("original_transaction_id"); + userSubscriptionNotifyDao.setSubscriptionId(receiptId, originalTransactionId); + userSubscriptionService.payFailSystemMessage(originalTransactionId, UserSubscription.Platform.APPLE); + userSubscriptionService.completeNotify(receiptId); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void doWhenPay(Long receiptId, String playlod) { + JSONObject result = JSON.parseObject(playlod); + String notification_type = result.getString("notification_type"); +// if(!"DID_RENEW".equals(notification_type)||"INTERACTIVE_RENEWAL".equals(notification_type) || "DID_RECOVER".equals(notification_type)){ +// return ; +// } + //处理订阅升级立马扣费的情况, 这个时候需要处理计算buff发放的情况 TODO 这儿是否会导致处理buff发放的问题?? + if (!"DID_RENEW".equals(notification_type) && !"INTERACTIVE_RENEWAL".equals(notification_type)) { + return; + } + + String latest_receipt = result.getJSONObject("unified_receipt").getString("latest_receipt"); + String env = result.getString("environment"); + JSONObject latestReceiptInfo = checkAndGeLatestReceiptInfoByEnv(latest_receipt, env); + + log.info("===> latestReceiptInfo : {}", latestReceiptInfo); + + String originalTransactionId = latestReceiptInfo.getString("original_transaction_id"); + String transactionId = latestReceiptInfo.getString("transaction_id"); + userSubscriptionNotifyDao.setSubscriptionId(receiptId, originalTransactionId); + + String productId = latestReceiptInfo.getString("product_id"); + //过期时间 + Long expiresDateMs = Long.valueOf(latestReceiptInfo.getString("expires_date_ms")); + //订购时间 + Long purchase_date_ms = Long.valueOf(latestReceiptInfo.getString("purchase_date_ms")); + + IOSReceiptInput iosReceiptInput = IOSReceiptInput.builder() + .originalTransactionId(originalTransactionId) + .transactionId(transactionId) + .productId(productId) + .expires_date_ms(expiresDateMs) + .purchase_date_ms(purchase_date_ms).build(); + + userSubscriptionService.bind(iosReceiptInput.getSubscriptionId(), UserSubscription.Platform.APPLE, iosReceiptInput.getExpiresDate(), iosReceiptInput.getPurchaseDate(), productId); + userSubscriptionService.completeNotify(receiptId); + } + + + //IOS 退订还没有调通 + @Transactional(rollbackFor = Exception.class) + @Override + public void doWhenCancel(Long receiptId, String playlod) { + JSONObject result = JSON.parseObject(playlod); + String notification_type = result.getString("notification_type"); + // String auto_renew_status = result.getString("auto_renew_status"); + if (!"CANCEL".equals(notification_type)) { + return; + } + String latest_receipt = result.getJSONObject("unified_receipt").getString("latest_receipt"); + String env = result.getString("environment"); + JSONObject latestReceiptInfo = checkAndGeLatestReceiptInfoByEnv(latest_receipt, env); + String originalTransactionId = latestReceiptInfo.getString("original_transaction_id"); + Long expiresDateMs = Long.valueOf(latestReceiptInfo.getString("expires_date_ms")); +// LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(expiresDateMs, 0, ZoneOffset.ofHours(8)); + userSubscriptionNotifyDao.setSubscriptionId(receiptId, originalTransactionId); + userSubscriptionService.cancel(originalTransactionId, UserSubscription.Platform.APPLE, expiresDateMs); + userSubscriptionService.completeNotify(receiptId); + } + + + public JSONObject checkAndGeLatestReceiptInfoByEnv(String receipt, String environment) { + String url = ""; + if ("Sandbox".equals(environment)) { + url = SANDBOX_VERIFY_RECEIPT_URL; + } else { + url = PRODUCT_VERIFY_RECEIPT_URL; + } + JSONObject request = new JSONObject(); + request.put("receipt-data", receipt); + request.put("password", password); + String str = netRetryUtils.postForObject(url, request, String.class); + log.info("iap verify receipt result: {}, receiptId: {}", str, receipt); + JSONObject result = JSON.parseObject(str); + int status = result.getInteger("status"); + + ToastResultCode.IAP_RECEIPT_STATUS_ERROR.check(status != SUCC_STATUS); + JSONObject receiptObj = result.getJSONObject("receipt"); + ToastResultCode.IAP_BUNDLE_ID_ERROR.check(!BUNDLE_ID.equals(receiptObj.getString("bundle_id"))); + JSONArray latestReceiptInfoArray = result.getJSONArray("latest_receipt_info"); + JSONObject latestReceiptInfo = latestReceiptInfoArray.getJSONObject(0); + return latestReceiptInfo; + } + + + public JSONObject checkAndGeLatestReceiptInfo(String receipt) { + JSONObject request = new JSONObject(); + request.put("receipt-data", receipt); + request.put("password", password); + String str = netRetryUtils.postForObject(verifyReceiptUrl, request, String.class); + log.info("iap verify receipt result: {}, receiptId: {}", str, receipt); + JSONObject result = JSON.parseObject(str); + + //生产环境沙盒充值标志 +// boolean sandBoxCharge = false; + int status = result.getInteger("status"); + //审核期间才会在生产环境校验沙盒收据 + if (status == SANDBOX_RECIPT) { + log.warn("生产环境沙盒充值校验逻辑"); + str = netRetryUtils.postForObject(SANDBOX_VERIFY_RECEIPT_URL, request, String.class); + log.warn("生产环境校验沙盒环境收据结果: {}", str); + result = JSON.parseObject(str); + status = result.getInteger("status"); +// sandBoxCharge = true; + } + ToastResultCode.IAP_RECEIPT_STATUS_ERROR.check(status != SUCC_STATUS); + JSONObject receiptObj = result.getJSONObject("receipt"); + ToastResultCode.IAP_BUNDLE_ID_ERROR.check(!BUNDLE_ID.equals(receiptObj.getString("bundle_id"))); + JSONArray latestReceiptInfoArray = result.getJSONArray("latest_receipt_info"); + JSONObject latestReceiptInfo = latestReceiptInfoArray.getJSONObject(0); + return latestReceiptInfo; + } + + /** + * 处理收据 + * 21000 App Store无法读取你提供的JSON数据 + * 21002 收据数据不符合格式 + * 21003 收据无法被验证 + * 21004 你提供的共享密钥和账户的共享密钥不一致 + * 21005 收据服务器当前不可用 + * 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中 + * 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证 + * 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证 + * + * @param req + * @return 是否处理完成 + */ + private boolean processReceipt(IapUploadReceiptReq req) { + JSONObject request = new JSONObject(); + request.put("receipt-data", req.getReceipt()); + request.put("password", password); + String str = netRetryUtils.postForObject(verifyReceiptUrl, request, String.class); + log.info("iap verify receipt result: {}, receiptId: {}", str, req.getReceiptId()); + JSONObject result = JSON.parseObject(str); + + //生产环境沙盒充值标志 +// boolean sandBoxCharge = false; + int status = result.getInteger("status"); + //审核期间才会在生产环境校验沙盒收据 + if (status == SANDBOX_RECIPT) { + log.warn("生产环境沙盒充值校验逻辑"); + str = netRetryUtils.postForObject(SANDBOX_VERIFY_RECEIPT_URL, request, String.class); + log.warn("生产环境校验沙盒环境收据结果: {}", str); + result = JSON.parseObject(str); + status = result.getInteger("status"); +// sandBoxCharge = true; + } + + if (status != SUCC_STATUS) { + log.info("收据不是成功状态{},{}", status, req.getReceiptId()); + //这里不应该是 return false才对吗? + return true; + } + + JSONObject receipt = result.getJSONObject("receipt"); + if (!BUNDLE_ID.equals(receipt.getString("bundle_id"))) { + log.error("收据应用ID对应不上"); + return true; + } + JSONArray inApps = receipt.getJSONArray("in_app"); + + for (int i = 0; i < inApps.size(); i++) { + JSONObject inApp = inApps.getJSONObject(i); + String productId = inApp.getString("product_id"); + String transactionId = inApp.getString("transaction_id"); + log.info("transactionId: {}", transactionId); + String tradeNo = req.getTransactions().getString(transactionId); + log.info("tradeNo: {}", tradeNo); + + //找不到相应的tradeNo + if (StringUtils.isBlank(tradeNo)) { + continue; + } + + PayCharge payCharge = payChargeService.getByBundleIdAndProductIdAndPlatform(BUNDLE_ID, productId, "iOS"); + PayTrade payTrade = payTradeService.getByTradeNo(tradeNo); + + //沙盒环境 记录这条数据是由沙盒产生的 + if (SANDBOX_RECIPT == result.getInteger("status")) { + payTradeDao.setStanbox(1, payTrade.getId()); + } + +// if (sandBoxCharge) { +// List reviewAccountList = payConfigService.listReviewAccount(); +// if (reviewAccountList == null || !reviewAccountList.contains(payTrade.getSrcAccountId())) { +// log.error("生产环境校验沙盒环境收据,但交易发起账户不是iOS提审账户"); +// break; +// } +// //提审账号最大可充值金额校验 +// if (payTradeService.sumChargeOccurAmount(payTrade.getSrcAccountId()) > AccountConstant.REVIEW_ACCOUNT_MAX_CHARGE_AMOUNT) { +// log.error("The top-up amount on the day cannot exceed $1000.00"); +// break; +// } +// } + + if ((payCharge == null || payCharge.getPayAmount() == null) + || (payTrade == null || payTrade.getOccurAmount() == null) + || !Objects.equals(payCharge.getPayAmount(), payTrade.getOccurAmount())) { + log.error("付款金额不匹配"); + break; + } + + IapRecord iapRecord = iapRecordService.getByTransactionId(transactionId); + if (iapRecord == null) { + //事务 + TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition()); + try { + iapRecord = IapRecord.builder() + .tradeNo(tradeNo) + .transactionId(transactionId) + .receiptId(req.getReceiptId()) + .bundleId(BUNDLE_ID) + .productId(productId) + .result(inApp.toJSONString()) + .build(); + iapRecordService.save(iapRecord); + + //未处理的收据,可开始交易处理 + payTradeService.tradeHandle(payTrade, TradeEvent.PAYMENT); + transactionManager.commit(transactionStatus); + } catch (Exception e) { + transactionManager.rollback(transactionStatus); + log.error("iap receipt process error", e); + } + } + } + return true; + } + + + /** + * 生产环境沙盒充值逻辑, 提审账号校验 + * + * @param accountId + */ + private void sandBoxChargeCheck(Long accountId) { +// List reviewAccountList = payConfigService.listReviewAccount(); +// SysExceptionUtils.check(reviewAccountList == null || !reviewAccountList.contains(accountId), "", "生产环境校验沙盒环境收据,但交易发起账户不是iOS提审账户"); +// //提审账号最大可充值金额校验 +// BizExceptionUtils.check(payTradeService.sumChargeOccurAmount(accountId) > AccountConstant.REVIEW_ACCOUNT_MAX_CHARGE_AMOUNT, "", "The top-up amount on the day cannot exceed $1000.00"); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapUploadReceiptServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapUploadReceiptServiceImpl.java new file mode 100644 index 0000000..bd3f7ee --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapUploadReceiptServiceImpl.java @@ -0,0 +1,49 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.dao.IapUploadReceiptDao; +import com.sonic.lion.domain.entity.IapUploadReceipt; +import com.sonic.lion.service.IapUploadReceiptService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author: code + * @date: 2025/07/15 + * @Description: + * @version: 1.0.0 + */ +@Service +public class IapUploadReceiptServiceImpl implements IapUploadReceiptService { + + @Autowired + private IapUploadReceiptDao iapUploadReceiptDao; + + @Override + public IapUploadReceipt getById(Long id) { + return iapUploadReceiptDao.selectById(id); + } + + @Override + public Long save(@NonNull IapUploadReceipt iapUploadReceipt) { + iapUploadReceiptDao.insert(iapUploadReceipt); + return iapUploadReceipt.getId(); + } + + @Override + public int updateProcessed(@NonNull Long id, @NonNull Boolean processed) { + IapUploadReceipt updater = IapUploadReceipt.builder() + .id(id) + .processed(processed) + .build(); + return iapUploadReceiptDao.updateById(updater); + } + + @Override + public List list(Boolean processed, LocalDateTime startCreateTime, Integer num) { + return iapUploadReceiptDao.list(processed, startCreateTime, num); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapV2ServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapV2ServiceImpl.java new file mode 100644 index 0000000..2d14f01 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/IapV2ServiceImpl.java @@ -0,0 +1,564 @@ +package com.sonic.lion.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.dao.*; +import com.sonic.lion.domain.entity.*; +import com.sonic.lion.domain.input.IOSReceiptInput; +import com.sonic.lion.service.*; +import com.sonic.lion.utils.IOSConsumptionUtil; +import com.sonic.lion.utils.JwtUtils; +import com.sonic.lion.utils.NetRetryUtils; +import com.sonic.lion.enums.ToastResultCode; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; + + +/** + * @author: code + * @date: 2025/06/22 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@Service("iapV2Service") +public class IapV2ServiceImpl implements IapV2Service, InitializingBean { + + @Value("${spring.profiles.active}") + private String active; + @Value("${apple.boundId}") + private String BUNDLE_ID; + @Value("${apple.password}") + private String password; +// @Value("${apple.environment:Sandbox}") + private String environment = "Sandbox"; + @Value("${apple.verifyReceiptUrl:}") + private String verifyReceiptUrl; + + private String verifySubscriptionUrl = "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions/"; +// private String verifySubscriptionUrl = "https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/"; + + @Autowired + private PayTradeService payTradeService; + @Autowired + private PayTradeDao payTradeDao; + @Autowired + private IapRecordService iapRecordService; + @Autowired + private UserSubscriptionService userSubscriptionService; + @Autowired + private UserSubscriptionDao userSubscriptionDao; + @Autowired + private NetRetryUtils netRetryUtils; + @Autowired + private AccountBuffBillDao accountBuffBillDao; + @Autowired + private AccountBuffService accountBuffService; + @Autowired + private IOSConsumptionUtil iosConsumptionUtil; + @Autowired + private AppleRefundRecordDao appleRefundRecordDao; + + @Autowired + private UserSubscriptionNotifyDao userSubscriptionNotifyDao; + + @Autowired + private ChannelBlacklistService channelBlacklistService; + + private static final int SUCC_STATUS = 0; + + private static final int SANDBOX_RECIPT = 21007; + + private static final String PRODUCT_VERIFY_RECEIPT_URL = "https://buy.itunes.apple.com/verifyReceipt"; + + private static final String SANDBOX_VERIFY_RECEIPT_URL = "https://sandbox.itunes.apple.com/verifyReceipt"; + + + @Override + public void afterPropertiesSet() { + if ("product".equals(active)) { + verifyReceiptUrl = PRODUCT_VERIFY_RECEIPT_URL; + } else { + verifyReceiptUrl = SANDBOX_VERIFY_RECEIPT_URL; + } + } + + //====通知类型==== + //SUBSCRIBED 如果subtype是INITIAL_BUY(首次购买),则用户首次通过“家人共享”购买或接收了对订阅的访问权限。如果是RESUBSCRIBE(重新购买/重新购买同一个组内的plan),则用户通过家庭共享重新订阅或接收了对同一订阅或同一订阅组内的另一个订阅的访问权限。 + //CONSUMPTION_REQUEST 一种通知类型,表明客户发起了应用内消费品(大师课/肤质检测次数购买)购买的退款请求 + //DID_RENEW 一种通知类型,与其一起subtype指示订阅已成功续订。如果subtype是BILLING_RECOVERY,则之前续订失败的过期订阅已成功续订。如果子状态为空(续订),则活动订阅已成功自动续订新的交易周期。为客户提供对订阅内容或服务的访问权限。 + //OFFER_REDEEMED 一种通知类型,与其 一起subtype指示用户兑换了促销优惠或优惠代码。如果subtype是INITIAL_BUY,则用户兑换了首次购买的优惠。如果是RESUBSCRIBE,则用户兑换了重新订阅非活动订阅的优惠。如果是UPGRADE,则用户兑换了升级其有效订阅的优惠,该优惠立即生效。如果是DOWNGRADE,则用户兑换了降级其有效订阅的优惠,该优惠将在下一个续订日期生效。 + //REFUND 一种通知类型,指示 AppStore已成功对消费品应用内购买、非消费品应用内购买、自动续订订阅或非续订订阅的交易进行退款。 + //DID_CHANGE_RENEWAL_STATUS 一种通知类型,与其一起subtype指示用户对订阅续订状态进行了更改。如果subtype=AUTO_RENEW_ENABLED,则用户重新启用订阅自动续订。如果是AUTO_RENEW_DISABLED,则用户禁用了订阅自动续费,或者用户申请退款后App Store禁用了订阅自动续费。 + //DID_CHANGE_RENEWAL_PREF 一种通知类型,与其一起subtype指示用户对其订阅计划进行了更改。如果subtype是UPGRADE,则用户升级了他们的订阅。升级立即生效,开始新的计费周期,用户将收到上一周期未使用部分的按比例退款。如果subtype是DOWNGRADE,则用户降级了他们的订阅。降级将在下一个续订日期生效,并且不会影响当前有效的计划。如果subtype为空,则用户将其续订首选项更改回当前订阅,从而有效地取消降级。 + //4. DID_FAIL_TO_RENEW 一种通知类型,与其一起subtype指示订阅由于计费问题而未能续订。订阅进入计费重试期。如果subtype是GRACE_PERIOD,则在宽限期内继续提供服务。如果为空,则说明订阅不在宽限期内,您可以停止提供订阅服务。 + //6. EXPIRED 如果subtype是VOLUNTARY,则订阅在用户禁用订阅续订后过期。如果subtype是BILLING_RETRY,则订阅已过期,因为计费重试期已结束,但没有成功的计费事务。如果是PRICE_INCREASE,则订阅已过期,因为用户不同意需要用户同意的价格上涨。如果是PRODUCT_NOT_FOR_SALE,则订阅已过期,因为在订阅尝试续订时该产品不可购买。 + //7. GRACE_PERIOD_EXPIRED 一种通知类型,指示计费宽限期已结束而无需续订订阅,因此您可以关闭对服务或内容的访问。通知用户他们的账单信息可能存在问题。 + //10. PRICE_INCREASE 一种通知类型,与其一起subtype表示系统已通知用户自动续订订阅价格上涨。 + //如果涨价需要用户同意,是subtype指PENDING用户没有对涨价做出回应,或者ACCEPTED用户已经同意涨价。 + //如果涨价不需要用户同意,那subtype就是ACCEPTED。 + //12. REFUND_DECLINED 一种通知类型,指示 AppStore 拒绝了应用开发者使用以下任一方法发起的退款请求 + //13. REFUND_REVERSED 一种通知类型,表明 App Store 由于客户提出的争议而撤销了之前授予的退款。如果您的应用因相关退款而撤销了内容或服务,则需要恢复它们。 + //此通知类型可适用于任何应用内购买类型:消耗型、非消耗型、非续订订阅和自动续订订阅。对于自动续订订阅,当 App Store 撤销退款时,续订日期保持不变。 + //14. RENEWAL_EXTENDED 一种通知类型,指示 App Store 延长了特定订阅的订阅续订日期。您可以通过调用App Store Server API中的延长订阅续订日期或为所有活跃订阅者延长订阅续订日期来请求订阅续订日期延期。 + //15. RENEWAL_EXTENSION 一种通知类型,与其一起subtype表示 AppStore 正在尝试通过调用为所有活跃订阅者延长订阅续订日期 来延长您请求的订阅续订日期。如果subtype是SUMMARY,则 AppStore 已完成为所有符合条件的订阅者延长续订日期。 + //16. REVOKE指示用户有权通过“家人共享”进行应用内购买的通知类型不再可通过共享进行。当购买者对其购买禁用“家庭共享”、购买者(或家庭成员)离开家庭群组或购买者收到退款时,AppStore 会发送此通知。您的应用程序也会收到呼叫。家庭共享适用于非消耗性应用内购买和自动续订订阅。有关家庭共享的更多信息,请参阅在应用程序中支持家庭共享。 + + //====通知子类型==== + //1. ACCEPTED 适用于PRICE_INCREASE. 如果价格上涨需要客户同意,则带有此通知的通知表明客户同意订阅价格上涨;如果价格上涨不需要客户同意,则表明系统通知他们价格上涨。 + //2. AUTO_RENEW_DISABLED 适用于DID_CHANGE_RENEWAL_STATUS. 此类通知表明用户禁用了订阅自动续订,或者 App Store 在用户申请退款后禁用了订阅自动续订。 + //2. AUTO_RENEW_ENABLED 适用于DID_CHANGE_RENEWAL_STATUS. 包含此信息的通知表明用户启用了订阅自动续订。 + //3. BILLING_RECOVERY 适用于DID_RENEW. 出现此通知表示之前未能续订的过期订阅已成功续订。 + //4. BILLING_RETRY 适用于EXPIRED. 此类通知表明订阅已过期,因为订阅在计费重试期结束之前未能续订。 + //5. DOWNGRADE 适用于DID_CHANGE_RENEWAL_PREF. 包含此信息的通知表明用户降级了其订阅或交叉分级为具有不同持续时间的订阅。降级将在下一个续订日期生效。 + //6. FAILURE 适用于RENEWAL_EXTENSION. 包含此信息的通知表明单个订阅的订阅续订日期延期失败。有关详细信息,请参阅中的对象。有关请求的信息,请参阅延长所有活跃订阅者的订阅续订日期。 + //7. GRACE_PERIOD 适用于DID_FAIL_TO_RENEW. 包含此信息的通知表明订阅由于计费问题而无法续订。在宽限期内继续提供对订阅的访问。 + //8. INITIAL_BUY 适用于SUBSCRIBED. 包含此内容的通知表示用户首次购买订阅或用户首次通过家人共享获得对订阅的访问权限。 + //9. PENDING 适用于PRICE_INCREASE. 出现此通知表示系统已通知用户订阅价格上涨,但用户尚未接受。 + //10. PRICE_INCREASE 适用于EXPIRED. 此类通知表明订阅已过期,因为用户不同意涨价。 + //11. PRODUCT_NOT_FOR_SALE 适用于EXPIRED. 包含此内容的通知表明订阅已过期,因为在订阅尝试续订时无法购买该产品。 + //12. RESUBSCRIBE 适用于SUBSCRIBED. 带有此信息的通知表明用户通过家庭共享重新订阅或接收了对同一订阅或同一订阅组内的另一个订阅的访问权限。 + //13. SUMMARY 适用于RENEWAL_EXTENSION. 此通知表明 App Store 服务器已完成您为所有符合条件的订阅者延长订阅续订日期的请求。有关摘要详细信息,请参阅中的对象。有关请求的信息,请参阅延长所有活跃订阅者的订阅续订日期。 notificationTypesubtypesummaryresponseBodyV2DecodedPayload + //14. UPGRADE 适用于DID_CHANGE_RENEWAL_PREF. 包含此信息的通知表明用户已升级其订阅或交叉分级为具有相同持续时间的订阅。升级立即生效。 + //15. VOLUNTARY 适用于EXPIRED. 此类通知表明订阅在用户禁用订阅自动续订后已过期。 + + /** + * https://developer.apple.com/documentation/appstoreservernotifications/notification_type 相关链接. + * https://juejin.cn/post/7373944051294666778 + * @param receiptId + * @param payload + * @return + */ + @Override + public boolean handSubscribeWebhook(Long receiptId, String payload) { + //将处理订阅的异常给吃掉,不能影响后面的业务逻辑的执行 + try { + //修改是否自动续订的状态 + doWhenStatusChange(receiptId, payload); + } catch (Exception e) { + log.error("===> appleV2 webhook handler error notifyId2 : " + receiptId, e); + } + //处理 过期、取消订阅 + doWhenCancel(receiptId, payload); + //处理 订阅和续订成功的场景 + doWhenPay(receiptId, payload); + //处理 订阅失败的场景 + doWhenPayFail(receiptId, payload); + //处理 内购回调的场景 + doWenOnTimeCharge(receiptId, payload); + //处理 AppStore已成功对消费品应用内购买、非消费品应用内购买、自动续订订阅或非续订订阅的交易进行退款 + doWhenRefundV2(receiptId, payload); + //处理 客户发起了应用内消费品购买的退款请求 + doWhenConsumptionRequest(receiptId, payload); + return true; + } + + //当状态变更的时候 更新套餐 和 续订状态。 + @Transactional(rollbackFor = Exception.class) + @Override + public void doWhenStatusChange(Long receiptId, String payload) { + JSONObject result = JSON.parseObject(payload); + JSONObject renewalInfoJson = result.getJSONObject("data").getJSONObject("renewalInfo"); + //获取不到订阅信息,直接快速返回 + if(renewalInfoJson == null) { + return; + } + + String originalTransactionId = renewalInfoJson.getString("originalTransactionId"); + if (originalTransactionId == null) { + return; + } + //获取并校验订阅信息,并重新设置最新的值 + renewalInfoJson = checkAndGetRenewalInfo(originalTransactionId); + //当前订购的产品标识符 + String productId = renewalInfoJson.getString("productId"); + //获取是否自动续订的状态(0 自动续订已关闭。客户已关闭订阅的自动续订,并且在当前订阅期结束后不会续订。1 自动续订已开启。订阅将在当前订阅期结束时续订。) + Boolean autoRenew = "1".equals(renewalInfoJson.getString("autoRenewStatus")); + //将是否自动续订的状态更新到数据库中 + userSubscriptionService.updateStatusByThird(originalTransactionId, UserSubscription.Platform.APPLE, productId, autoRenew); + if (!autoRenew) { + userSubscriptionService.cancel(originalTransactionId, UserSubscription.Platform.APPLE, null); + } + userSubscriptionService.completeNotify(receiptId); + } + + + @Transactional(rollbackFor = Exception.class) + @Override + public void doWhenConsumptionRequest(Long receiptId, String payload) { + JSONObject result = JSON.parseObject(payload); + JSONObject transactionInfoJson = result.getJSONObject("data").getJSONObject("transactionInfo"); + //获取通知类型 + String notificationType = result.getString("notificationType"); + + if (!"CONSUMPTION_REQUEST".equals(notificationType)) { + return; + } + //环境不匹配,不允许处理 + String resultEvn = result.getString("environment"); + String resultBundleId = result.getString("bundleId"); + ToastResultCode.IAP_RECEIPT_STATUS_ERROR.check(!environment.equals(resultEvn)); + ToastResultCode.IAP_BUNDLE_ID_ERROR.check(!BUNDLE_ID.equals(resultBundleId)); + //校验是IOS服务器 + if ("PROD".equals(resultEvn) && !result.getString("password").equals(password)) { + return; + } + String originalTransactionId = transactionInfoJson.getString("originalTransactionId"); + IapRecord record = iapRecordService.getByTransactionId(originalTransactionId); + if (record == null) { + log.info("===> doWhenConsumptionRequest IapRecord not found , receiptId={}", receiptId); + return; + } + int count = appleRefundRecordDao.selectCount(Wrappers.lambdaQuery() + .eq(AppleRefundRecord::getTransactionId, originalTransactionId)); + //已经存在的数据 不再处理 + if (count > 0) { + log.info("===> doWhenConsumptionRequest IapRecord data found , receiptId={}", receiptId); + return; + } + String tradeNo = record.getTradeNo(); + PayTrade payTrade = payTradeService.getByTradeNo(tradeNo); + + int consumptionStatus = 0; + //购买得到的buff + Long getBuffAmount = payTrade.getAmount(); + //这期间 消费了的buff + Long sumConsumptionTotal = accountBuffBillDao.sumConsumption(payTrade.getPayTime(), LocalDateTime.now(), payTrade.getSrcAccountId()); + if (sumConsumptionTotal == 0) { //没有消费 扣除buff 并发送系统消息。 + consumptionStatus = 1; //不消耗应用内购买 + } else if (sumConsumptionTotal > 0 && sumConsumptionTotal < getBuffAmount) { + consumptionStatus = 2; //应用内购买被部分消费 + } else { + consumptionStatus = 3; //应用内购买已完全消耗 + } + + + /** + * {"transaction_id":"200001174368839","original_purchase_date":"2021-12-25 15:42:33 Etc/GMT", + * "in_app_ownership_type":"PURCHASED","quantity":"1","original_transaction_id":"200001174368839", + * "purchase_date_pst":"2021-12-25 07:42:33 America/Los_Angeles","original_purchase_date_ms": + * "1640446953000","purchase_date_ms":"1640446953000","product_id":"gg.epal.buff_0004", + * "original_purchase_date_pst":"2021-12-25 07:42:33 America/Los_Angeles", + * "is_trial_period":"false","purchase_date":"2021-12-25 15:42:33 Etc/GMT"} + */ + + JSONObject latestReceiptInfo = JSONObject.parseObject(record.getResult()); + //订购时间 + Long purchase_date_ms = Long.valueOf(latestReceiptInfo.getString("purchase_date_ms")); + LocalDateTime purchaseDate = LocalDateTime.ofEpochSecond(purchase_date_ms / 1000, 0, ZoneOffset.ofHours(8)); + + + Integer totalBuy = payTradeDao.countTotalBuy(payTrade.getSrcAccountId()); + Integer totalRefund = payTradeDao.countTotalRefund(payTrade.getSrcAccountId()); + //发送IOS 消费. + String json = iosConsumptionUtil.sendConsumption(originalTransactionId, 1, consumptionStatus, totalBuy, totalRefund, 1); + +// accountBuffService.fronzenBalanceAndSendMessage(user.getId(), getBuffAmount); //冻结 金额并 发送消息 + + //开始处理 冻结 + AppleRefundRecord.FronzenStatus fronzenStatus = accountBuffService.fronzenBalanceAndSendMessage(payTrade.getSrcAccountId(), payTrade.getAmount()); + + AppleRefundRecord appleRefundRecord = AppleRefundRecord.builder().productId(latestReceiptInfo.getString("product_id")) + .platform(AppleRefundRecord.Platform.IOS).bizType(BizType.CHARGE) + .content(payload) + .transactionId(originalTransactionId).cancellationDate(LocalDateTime.now()).purchaseDate(purchaseDate).build(); + + appleRefundRecord.setAmount(payTrade.getOccurAmount()); + appleRefundRecord.setUserId(payTrade.getSrcAccountId()); + appleRefundRecord.setTradeNo(tradeNo); + appleRefundRecord.setBuff(payTrade.getAmount()); + + appleRefundRecord.setFronzenStatus(fronzenStatus); + AccountBuff accountBuff = accountBuffService.getByUid(payTrade.getSrcAccountId()); + appleRefundRecord.setAfterFronzenBuff(accountBuff.getBalance()); + appleRefundRecord.setInsertTime(LocalDateTime.now()); + appleRefundRecordDao.insert(appleRefundRecord); + //将用户加入渠道黑名单中 + channelBlacklistService.addBlacklist(appleRefundRecord.getUserId(), PayChannel.STRIPE.name()); + + userSubscriptionService.updateNotifyExtend(receiptId, appleRefundRecord.getId(), json); + userSubscriptionService.completeNotify(receiptId); + } + + + private void doWhenRefund(Long receiptId, String playload) { + JSONObject result = JSON.parseObject(playload); + String notification_type = result.getString("notification_type"); + if (!"REFUND".equals(notification_type)) { + return; + } + //校验是IOS服务器 + if (!result.getString("password").equals(password)) { + return; + } + + JSONObject latestReceiptInfo = result.getJSONObject("unified_receipt").getJSONArray("latest_receipt_info").getJSONObject(0); + + + Long cancellation_date_ms = Long.valueOf(latestReceiptInfo.getString("cancellation_date_ms")); + LocalDateTime cancellation_date = LocalDateTime.ofEpochSecond(cancellation_date_ms / 1000, 0, ZoneOffset.ofHours(8)); + + + Long purchase_date_ms = Long.valueOf(latestReceiptInfo.getString("purchase_date_ms")); + LocalDateTime purchase_date = LocalDateTime.ofEpochSecond(purchase_date_ms / 1000, 0, ZoneOffset.ofHours(8)); + + String transaction_id = latestReceiptInfo.getString("transaction_id"); + AppleRefundRecord appleRefundRecord = AppleRefundRecord.builder().productId(latestReceiptInfo.getString("product_id")) + .platform(AppleRefundRecord.Platform.IOS).bizType(BizType.CHARGE) + .content(playload) + .transactionId(transaction_id).cancellationDate(cancellation_date).purchaseDate(purchase_date).build(); + + + IapRecord record = iapRecordService.getByTransactionId(transaction_id); + if (record != null) { + String tradeNo = record.getTradeNo(); + PayTrade payTrade = payTradeService.getByTradeNo(tradeNo); + appleRefundRecord.setAmount(payTrade.getOccurAmount()); + appleRefundRecord.setUserId(payTrade.getSrcAccountId()); + appleRefundRecord.setTradeNo(tradeNo); + appleRefundRecord.setBuff(payTrade.getAmount()); + } + appleRefundRecord.setInsertTime(LocalDateTime.now()); + appleRefundRecordDao.insert(appleRefundRecord); + //将用户加入渠道黑名单中 + channelBlacklistService.addBlacklist(appleRefundRecord.getUserId(), PayChannel.STRIPE.name()); + userSubscriptionService.completeNotify(receiptId); + } + + private void doWenOnTimeCharge(Long receiptId, String payload) { + try { + JSONObject result = JSON.parseObject(payload); + //获取通知类型 + String notificationType = result.getString("notificationType"); + + if (!"ONE_TIME_CHARGE".equals(notificationType)) { + return; + } + //环境不匹配,不允许处理 + String resultEvn = result.getString("environment"); + String resultBundleId = result.getString("bundleId"); + ToastResultCode.IAP_RECEIPT_STATUS_ERROR.check(!environment.equals(resultEvn)); + ToastResultCode.IAP_BUNDLE_ID_ERROR.check(!BUNDLE_ID.equals(resultBundleId)); + //生产环境密钥校验配置 + if ("PROD".equals(resultEvn) && !result.getString("password").equals(password)) { + return; + } + JSONObject transactionInfoJson = result.getJSONObject("data").getJSONObject("transactionInfo"); + //获取退款对应的原始交易号 + String transactionId = transactionInfoJson.getString("transactionId"); + //更新请求,将交易号写到订阅id字段中 + userSubscriptionService.updateStatusAndSubscriptionId(receiptId, transactionId); + } catch (Exception e) { + log.info("===> doWenOnTimeCharge error"); + e.printStackTrace(); + } + } + + /** + * 处理收到渠道方的退款回调数据 + * @param payload + */ + private void doWhenRefundV2(Long receiptId, String payload) { + try { + JSONObject result = JSON.parseObject(payload); + JSONObject transactionInfoJson = result.getJSONObject("data").getJSONObject("transactionInfo"); + //获取通知类型 + String notificationType = result.getString("notificationType"); + + if (!"REFUND".equals(notificationType)) { + return; + } + //校验是IOS服务器 + if (!result.getString("password").equals(password)) { + return; + } + //获取退款对应的原始交易号 + String originalTransactionId = transactionInfoJson.getString("originalTransactionId"); + //根据交易号查询出对应的退款数据 + AppleRefundRecord appleRefundRecord = appleRefundRecordDao.selectOne(Wrappers.lambdaQuery() + .select(AppleRefundRecord::getId) + .eq(AppleRefundRecord::getTransactionId, originalTransactionId)); + if(appleRefundRecord == null) { + log.info("===> doWhenRefundV2 data not found"); + return; + } + //将请求的数据给更新过来 + userSubscriptionService.updateNotifyExtend(receiptId, appleRefundRecord.getId(), null); + } catch (Exception e) { + log.info("===> doWhenRefundV2 error"); + e.printStackTrace(); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void doWhenPayFail(Long receiptId, String payload) { + JSONObject result = JSON.parseObject(payload); + JSONObject transactionInfoJson = result.getJSONObject("data").getJSONObject("transactionInfo"); + JSONObject renewalInfoJson = result.getJSONObject("data").getJSONObject("renewalInfo"); + //获取通知类型 + String notificationType = result.getString("notificationType"); + String subtype = result.getString("subtype"); + + // DID_FAIL_TO_RENEW类型下:如果subtype是GRACE_PERIOD,则在宽限期内继续提供服务。如果为空,则说明订阅不在宽限期内,您可以停止提供订阅服务。 + if (!"DID_FAIL_TO_RENEW".equals(notificationType) || StringUtils.isNotEmpty(subtype)) { + return; + } + //获取原始交易号 + String originalTransactionId = renewalInfoJson.getString("originalTransactionId"); + //获取并校验订阅信息,并重新设置最新的值 + renewalInfoJson = checkAndGetRenewalInfo(originalTransactionId); + //修改当前通知的交易号为原始交易号 + userSubscriptionNotifyDao.setSubscriptionId(receiptId, originalTransactionId); + //续订失败,发送系统消息 + userSubscriptionService.payFailSystemMessage(originalTransactionId, UserSubscription.Platform.APPLE); + //更新通知日志数据的处理状态 + userSubscriptionService.completeNotify(receiptId); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void doWhenPay(Long receiptId, String payload) { + JSONObject result = JSON.parseObject(payload); + JSONObject transactionInfoJson = result.getJSONObject("data").getJSONObject("transactionInfo"); + JSONObject renewalInfoJson = result.getJSONObject("data").getJSONObject("renewalInfo"); + //获取通知类型 + String notificationType = result.getString("notificationType"); + + //SUBSCRIBED 首次订阅、变更套餐(原来订阅1个月,换成订阅3个月的 会先失效掉1个月的,再订阅3个月的)、DID_RENEW 续订 + if (!"SUBSCRIBED".equals(notificationType) && !"DID_RENEW".equals(notificationType)) { + return; + } + //获取原始交易号 + String originalTransactionId = renewalInfoJson.getString("originalTransactionId"); + //获取并校验订阅信息,并重新设置最新的值 + renewalInfoJson = checkAndGetRenewalInfo(originalTransactionId); + //获取当前的交易号 + String transactionId = transactionInfoJson.getString("transactionId"); + //更新原始交易号 + userSubscriptionNotifyDao.setSubscriptionId(receiptId, originalTransactionId); + + //获取商品ID + String productId = renewalInfoJson.getString("productId"); + //订购时间 + Long purchaseDateMs = Long.valueOf(renewalInfoJson.getString("recentSubscriptionStartDate")); + //过期时间 + Long expiresDateMs = Long.valueOf(renewalInfoJson.getString("renewalDate")); + + //构造收据数据 + IOSReceiptInput iosReceiptInput = IOSReceiptInput.builder() + .originalTransactionId(originalTransactionId) + .transactionId(transactionId) + .productId(productId) + .expires_date_ms(expiresDateMs) + .purchase_date_ms(purchaseDateMs).build(); + //绑定订阅关系,并发送mq消息通知业务系统 + userSubscriptionService.bind(iosReceiptInput.getSubscriptionId(), UserSubscription.Platform.APPLE, iosReceiptInput.getExpiresDate(), iosReceiptInput.getPurchaseDate(), productId); + //更新订阅通知处理状态 + userSubscriptionService.completeNotify(receiptId); + } + + + //IOS 退订还没有调通 + @Transactional(rollbackFor = Exception.class) + @Override + public void doWhenCancel(Long receiptId, String payload) { + JSONObject result = JSON.parseObject(payload); + JSONObject renewalInfoJson = result.getJSONObject("data").getJSONObject("renewalInfo"); + //获取通知类型 + String notificationType = result.getString("notificationType"); + + //EXPIRED 出现在过期或从1个月切换成3个月时,先过期退款,然后再重新订阅 + if (!"EXPIRED".equals(notificationType)) { + return; + } + //获取原始交易号 + String originalTransactionId = renewalInfoJson.getString("originalTransactionId"); + //获取并校验订阅信息,并重新设置最新的值 + renewalInfoJson = checkAndGetRenewalInfo(originalTransactionId); + //更新原始交易号 + userSubscriptionNotifyDao.setSubscriptionId(receiptId, originalTransactionId); + + //过期时间 + Long expiresDateMs = Long.valueOf(renewalInfoJson.getString("renewalDate")); + //更新过期时间,并发送mq消息通知业务系统 + userSubscriptionService.cancel(originalTransactionId, UserSubscription.Platform.APPLE, expiresDateMs); + //更新日志数据处理状态 + userSubscriptionService.completeNotify(receiptId); + } + + + public JSONObject checkAndGeLatestReceiptInfoByEnv(String receipt, String environment) { + String url; + if ("Sandbox".equals(environment)) { + url = SANDBOX_VERIFY_RECEIPT_URL; + } else { + url = PRODUCT_VERIFY_RECEIPT_URL; + } + JSONObject request = new JSONObject(); + request.put("receipt-data", receipt); + request.put("password", password); + String str = netRetryUtils.postForObject(url, request, String.class); + log.info("iap verify receipt result: {}, receiptId: {}", str, receipt); + JSONObject result = JSON.parseObject(str); + int status = result.getInteger("status"); + + ToastResultCode.IAP_RECEIPT_STATUS_ERROR.check(status != SUCC_STATUS); + JSONObject receiptObj = result.getJSONObject("receipt"); + ToastResultCode.IAP_BUNDLE_ID_ERROR.check(!BUNDLE_ID.equals(receiptObj.getString("bundle_id"))); + JSONArray latestReceiptInfoArray = result.getJSONArray("latest_receipt_info"); + JSONObject latestReceiptInfo = latestReceiptInfoArray.getJSONObject(0); + return latestReceiptInfo; + } + + + /** + * 获取并校验订阅信息 + * @param originalTransactionId + * @return + */ + public JSONObject checkAndGetRenewalInfo(String originalTransactionId) { + try { + String token = JwtUtils.getAppleJwt(BUNDLE_ID); + String url = verifySubscriptionUrl + originalTransactionId; + HttpResponse response = Unirest.get(url) + .header("Authorization", "Bearer " + token) + .asString(); + int status = response.getStatus(); + if (status != 200) { + throw new RuntimeException("HTTP Error: " + status); + } + String result = response.getBody(); + log.info("===> checkAndGetRenewalInfo result : {}", result); + JSONObject jsonObject = JSON.parseObject(result); + //环境不匹配,不允许处理 + String resultEvn = jsonObject.getString("environment"); + String resultBundleId = jsonObject.getString("bundleId"); + ToastResultCode.IAP_RECEIPT_STATUS_ERROR.check(!environment.equals(resultEvn)); + ToastResultCode.IAP_BUNDLE_ID_ERROR.check(!BUNDLE_ID.equals(resultBundleId)); + //获取订阅基础信息 + String signedRenewalInfo = jsonObject.getJSONArray("data").getJSONObject(0).getJSONArray("lastTransactions").getJSONObject(0).getString("signedRenewalInfo"); + log.info("===> checkAndGetRenewalInfo signedRenewalInfo : {}", signedRenewalInfo); + return JSONObject.parseObject(JwtUtils.getJwtBody(signedRenewalInfo)); + } catch (Exception e) { + log.error("===> checkAndGetRenewalInfo error : ", e); + ToastResultCode.SYSTEM_EXCEPTION.check(true); + } + return null; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/MemberPrivDictServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/MemberPrivDictServiceImpl.java new file mode 100644 index 0000000..ecfd3e0 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/MemberPrivDictServiceImpl.java @@ -0,0 +1,23 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.lion.dao.MemberPrivDictDao; +import com.sonic.lion.domain.MemberPrivDict; +import com.sonic.lion.service.MemberPrivDictService; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; + +/** + * 会员特权字典表 服务实现类 + */ +@Service +public class MemberPrivDictServiceImpl extends ServiceImpl implements MemberPrivDictService { + + @Override + public List getMemberPrivList() { + return list(Wrappers.lambdaQuery().eq(MemberPrivDict::getIsDelete, 0).orderByAsc(MemberPrivDict::getSort)); + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/MemberServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/MemberServiceImpl.java new file mode 100644 index 0000000..3ea0bc2 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/MemberServiceImpl.java @@ -0,0 +1,59 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.sonic.lion.dao.UserSubscriptionDao; +import com.sonic.lion.domain.entity.UserSubscription; +import com.sonic.lion.domain.input.PlatformGiftInput; +import com.sonic.lion.domain.output.MemberDetailOutput; +import com.sonic.lion.service.MemberPrivDictService; +import com.sonic.lion.service.MemberService; +import com.sonic.lion.service.PayTradeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Service +public class MemberServiceImpl implements MemberService { + + @Autowired + private UserSubscriptionDao userSubscriptionDao; + @Autowired + private MemberPrivDictService memberPrivDictService; + @Autowired + private PayTradeService payTradeService; + + @Override + public MemberDetailOutput memberDetail(Long userId) { + MemberDetailOutput output = new MemberDetailOutput(); + //当前用户会员信息 + output.setUserMemberInfo(userSubscriptionDao.getByUserId(userId)); + //用户会员权限列表 + output.setMemberPrivList(memberPrivDictService.getMemberPrivList()); + return output; + } + + @Override + public Integer userMemberGiftBuffJob() { + //查询所有未过期的用户信息 + List userIdList = userSubscriptionDao.getNotExpiredSubscriptionList(); + if (CollectionUtils.isEmpty(userIdList)) { + return 0; + } + for (Long userId : userIdList) { + //赠送5个CrushCoin + PlatformGiftInput platformGiftInput = new PlatformGiftInput(); + platformGiftInput.setPlatform("1"); + platformGiftInput.setUid(userId); + platformGiftInput.setName(PlatformGiftInput.Type.VIP_BUFF_GIFT.getBizType().getDesc()); + platformGiftInput.setCreateTime(LocalDateTime.now()); + platformGiftInput.setAmount(500L); + platformGiftInput.setType(PlatformGiftInput.Type.VIP_BUFF_GIFT); + payTradeService.platformGift(platformGiftInput); + } + return userIdList.size(); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayAccountFundThirdServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayAccountFundThirdServiceImpl.java new file mode 100644 index 0000000..5a3457d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayAccountFundThirdServiceImpl.java @@ -0,0 +1,174 @@ +package com.sonic.lion.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.dao.PayAccountFundThirdDao; +import com.sonic.lion.dao.PayAccountThirdBindDao; +import com.sonic.lion.domain.entity.PayAccountFundThird; +import com.sonic.lion.domain.entity.PayAccountThirdBind; +import com.sonic.lion.domain.enums.UserThirdStatus; +import com.sonic.lion.domain.input.AccountGenerateInput; +import com.sonic.lion.domain.input.BindBankCardInput; +import com.sonic.lion.domain.input.ChannelBindBankCardInput; +import com.sonic.lion.domain.input.ChannelCreateCustomerInput; +import com.sonic.lion.domain.output.ChannelBindBankCardOutput; +import com.sonic.lion.domain.output.ChannelCreateCustomerOutput; +import com.sonic.lion.domain.output.CreateBeneficiaryOutput; +import com.sonic.lion.enums.BindDataType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.ThirdAccountType; +import com.sonic.lion.service.PayAccountFundThirdService; +import com.sonic.lion.service.PayCallChannelService; +import com.sonic.sdk.api.utils.AesEncodeUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +/** + * @author: code + * @date: 2025/05/12 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@Service +public class PayAccountFundThirdServiceImpl implements PayAccountFundThirdService { + + @Autowired + private PayCallChannelService payCallChannelService; + + @Autowired + private PayAccountFundThirdDao payAccountFundThirdDao; + + @Autowired + private PayAccountThirdBindDao payAccountThirdBindDao; + + public static final String SECRET_KEY = "a3580eb6999ebf2e07ba45237c0de889"; + + @Override + public PayAccountFundThird getByAccountIdAndAppType(Long accountId, ThirdAccountType thirdAccountType) { + if (accountId == null || thirdAccountType == null) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(PayAccountFundThird::getAccountId, accountId) + .eq(PayAccountFundThird::getAppType, thirdAccountType) + .orderByDesc(PayAccountFundThird::getId).last(" limit 1"); + return payAccountFundThirdDao.selectOne(queryWrapper); + } + + @Override + public Long getAccountIdByOpenId(String openId, ThirdAccountType thirdAccountType) { + if (StringUtils.isBlank(openId) || thirdAccountType == null) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(PayAccountFundThird::getOpenId, openId) + .eq(PayAccountFundThird::getAppType, thirdAccountType) + .orderByDesc(PayAccountFundThird::getId).last(" limit 1"); + PayAccountFundThird payAccountFundThird = payAccountFundThirdDao.selectOne(queryWrapper); + if (payAccountFundThird == null) { + return null; + } + return payAccountFundThird.getAccountId(); + } + + @Override + public void generate(@NonNull AccountGenerateInput input) { + ChannelCreateCustomerInput channelCreateCustomerInput = ChannelCreateCustomerInput.builder() + .accountFundId(input.getAccountFundId()) + .addressLine1(input.getAddressLine1()) + .city(input.getCity()) + .country(input.getCountry()) + .email(input.getEmail()) + .firstName(input.getFirstName()) + .lastName(input.getLastName()) + .middleName(input.getMiddleName()) + .payChannel(input.getPayChannel()) + .postalCode(input.getPostalCode()) + .stateProvince(input.getStateProvince()) + .build(); + ChannelCreateCustomerOutput channelCreateCustomerOutput = payCallChannelService.callCreateCustomer(channelCreateCustomerInput); + String name = getName(input.getFirstName(), input.getMiddleName(), input.getLastName()); + PayAccountFundThird payAccountFundThird = PayAccountFundThird.builder() + .accountId(input.getUid()) + .accountFundId(input.getAccountFundId()) + .appType(ThirdAccountType.getWithdrawAccount(input.getPayChannel())) + .channelId(input.getPayChannel()) + .openId(channelCreateCustomerOutput.getOpenId()) + .status(UserThirdStatus.ENABLED) + .email(input.getEmail()) + .name(name) + .extend(channelCreateCustomerOutput.getResult()) + .build(); + payAccountFundThirdDao.insert(payAccountFundThird); + } + + @Override + public void bindBankCard(@NonNull BindBankCardInput input) { + + PayAccountFundThird payAccountFundThird = getByAccountIdAndAppType(input.getUid(), ThirdAccountType.getWithdrawAccount(input.getPayChannel())); + + //将银行卡绑定到渠道 + ChannelBindBankCardInput channelBindBankCardInput = ChannelBindBankCardInput.builder() + .thirdAccountOpenId(payAccountFundThird.getOpenId()) + .payChannel(input.getPayChannel()) + .cardNumber(input.getCardNumber()) + .cvv(input.getCvv()) + .dateOfExpiry(input.getDateOfExpiry()) + .build(); + ChannelBindBankCardOutput channelBindBankCardOutput = payCallChannelService.callBindBankCard(channelBindBankCardInput); + + //保存渠道openId和绑定关系 + PayAccountThirdBind payAccountThirdBind = PayAccountThirdBind.builder() + .accountThirdId(payAccountFundThird.getId()) + .bindDataType(BindDataType.BANK_CARD) + .thirdOpenId(channelBindBankCardOutput.getOpenId()) + .result(channelBindBankCardOutput.getResult()) + .build(); + payAccountThirdBindDao.insert(payAccountThirdBind); + } + + @Override + public PayAccountFundThird bindStripe(Long uid, String customerId) { + PayAccountFundThird third = PayAccountFundThird.builder() + .accountId(uid) + .channelId(PayChannel.STRIPE) + .appType(ThirdAccountType.STRIPE_PAYMENT) + .openId(customerId) + .name(uid.toString()) + .email(null) + //加密存储 (付款方式、国家、货币类型、银行卡号 等) + .status(UserThirdStatus.ENABLED) + .build(); + payAccountFundThirdDao.insert(third); + return third; + } + + + private String getName(String firstName, String middleName, String lastName) { + if (StringUtils.isBlank(middleName)) { + return firstName + " " + lastName; + } else { + return firstName + " " + middleName + " " + lastName; + } + } + + + public static void main(String[] args) throws Exception { + CreateBeneficiaryOutput output = new CreateBeneficiaryOutput(); + output.setBeneficiaryId("bf40c0c2-c064-4798-a6a0-ec4125f58a35"); + output.setBankName("GOLDMAN SACHS BANK USA (FORMERLY THE GOLDMAN SACHS TRUST COMPANY)"); + output.setLocalClearingSystem("ACH"); + output.setBankCountryCode("US"); + output.setAccountNumber("555552455"); + output.setAccountCurrency("USD"); + output.setAccountName("ttt"); + String abc = AesEncodeUtil.encrypt(JSONObject.toJSONString(output), SECRET_KEY); + System.out.println(abc); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayCallChannelServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayCallChannelServiceImpl.java new file mode 100644 index 0000000..15c488f --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayCallChannelServiceImpl.java @@ -0,0 +1,488 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.common.exception.BizException; +import com.sonic.common.exception.SysException; +import com.sonic.lion.dao.*; +import com.sonic.lion.domain.entity.*; +import com.sonic.lion.domain.enums.*; +import com.sonic.lion.domain.input.*; +import com.sonic.lion.domain.output.*; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.PayGenrtatorCodeType; +import com.sonic.lion.enums.ToastResultCode; +import com.sonic.lion.service.PayCallChannelService; +import com.sonic.lion.service.PayChannelRouterService; +import com.sonic.lion.service.PayChannelService; +import com.sonic.lion.utils.KeyGenerator; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * @author: code + * @date: 2025/05/13 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@Service +public class PayCallChannelServiceImpl implements PayCallChannelService { + + @Value("${stripe.withdrawFeeRate}") + private BigDecimal withdrawFeeRate; + + @Autowired + private PayCallChannelRecordDao payCallChannelRecordDao; + + @Autowired + private PayChannelBillDao payChannelBillDao; + + @Autowired + private PayChannelRouterService payChannelRouterService; + + @Autowired + private RedissonClient redissonClient; + + @Autowired + private PayTradeDao payTradeDao; + + @Autowired + private ProcessingChargeDao processingChargeDao; + + @Autowired + private ProcessingWithdrawDao processingWithdrawDao; + + + private static final long CHECK_RECORD_DAY = 30L; + + private static final String PAYOUT_LOCK_PREFIX = "pay:channel:payout:tradeNo:"; + + + @Override + public PayCallChannelRecord get(Long id) { + if (id == null) { + return null; + } + return payCallChannelRecordDao.selectById(id); + } + + @Override + public PayCallChannelRecord getByTradeNoAndBizTypeLast(String tradeNo, BizType bizType) { + if (StringUtils.isBlank(tradeNo) || bizType == null) { + return null; + } + return payCallChannelRecordDao.selectByTradeNoAndBizTypeLast(tradeNo, bizType); + } + + @Override + public List waitCheckRecord() { + LocalDateTime startCreateTime = LocalDateTime.now().minusDays(CHECK_RECORD_DAY); + return payCallChannelRecordDao.selectWillCheckRecord(startCreateTime); + } + + + public static void main(String[] args) { + LocalDateTime startCreateTime = LocalDateTime.now().minusDays(CHECK_RECORD_DAY); + System.out.println(startCreateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + } + + /** + * 防止调用记录被回滚掉,此方法不能加事务 + * + * @param input + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ChannelPayoutOutput callPayout(@NonNull ChannelPayoutInput input) { + ChannelPayoutOutput result = ChannelPayoutOutput.builder().build(); + //加锁 + RLock rLock = redissonClient.getLock(PAYOUT_LOCK_PREFIX + input.getTradeNo()); + try { + if (!rLock.tryLock(0L, 60L, TimeUnit.SECONDS)) { + result.setStatus(CallChannelStatus.CANCEL); + log.info("获得锁失败取消 , tradeNo: {}, time: {}", rLock.getName(), System.currentTimeMillis()); + return result; + } + } catch (InterruptedException e) { + result.setStatus(CallChannelStatus.CANCEL); + log.info("阻塞异常取消 , tradeNo: {}, time: {}", rLock.getName(), System.currentTimeMillis()); + return result; + } + + log.info("获得锁, tradeNo: {}, time: {}", rLock.getName(), System.currentTimeMillis()); + try { + PayCallChannelRecord lastRecord = getByTradeNoAndBizTypeLast(input.getTradeNo(), BizType.WITHDRAW); + //已经调用过一次渠道,不发起请求 + if (lastRecord != null) { + result.setStatus(CallChannelStatus.CANCEL); + log.info("重复发起 取消, tradeNo: {}, time: {}", rLock.getName(), System.currentTimeMillis()); + return result; + } + + PayCallChannelRecord record = PayCallChannelRecord.builder() + .tradeNo(input.getTradeNo()) + .bizType(input.getBizType()) + .channel(input.getPayChannel()) + .status(CallChannelStatus.PROCESSING) + .amount(input.getAmount()) + .checkNum(0) + .nextCheckTime(LocalDateTime.now().plusMinutes(5)) + .id(IdWorker.getId()) + .build(); + + this.save(record); + + if(BizType.WITHDRAW.equals(input.getPayTrade().getBizType())){ + //创建渠道调用记录的时候同时创建一条待 处理记录 + ProcessingWithdraw processingWithdraw = ProcessingWithdraw.builder() + .tradeNo(input.getPayTrade().getTradeNo()) + .callChannelRecordId(record.getId()) + .payChannel(input.getPayTrade().getPayChannel()) + .createTime(LocalDateTime.now()) + .handCount(0) + .nextHandTime(LocalDateTime.now()) + .build(); + processingWithdrawDao.insert(processingWithdraw); + } + + input.setSubmitId(record.getId().toString()); + PayChannelService payChannelService = payChannelRouterService.getPayChannelService(input.getPayChannel()); + ChannelPayoutOutput payoutOutput = payChannelService.payout(input); + log.info("提现调用渠道结果, {}", payoutOutput); + + //更新状态 + PayCallChannelRecord updater = PayCallChannelRecord.builder() + .id(record.getId()) + .status(payoutOutput.getStatus()) + .batchId(payoutOutput.getBatchId()) + .transactionId(payoutOutput.getTransactionId()) + .result(payoutOutput.getResult()) + .build(); + payCallChannelRecordDao.updateById(updater); + return payoutOutput; + } catch (Exception e) { + //因为提现重试风险较高,所以捕获异常后只做log记录,留待人工排查,不做重试 + log.error("call payout error", e); + result.setStatus(CallChannelStatus.CANCEL); + return result; + } finally { + try { + log.info("释放锁, tradeNo: {}, time: {}", rLock.getName(), System.currentTimeMillis()); + rLock.unlock(); + }catch (Exception e){ + + } + } + } + + + @Override + public ChannelPaymentOutput callPayment(@NonNull ChannelPaymentInput input) { + PayTrade payTrade = input.getPayTrade(); + PayCallChannelRecord lastRecord = getByTradeNoAndBizTypeLast(payTrade.getTradeNo(), payTrade.getBizType()); + //该笔支付没有调用第三方的记录,直接调用并记录 + if (lastRecord == null || (lastRecord.getStatus() == CallChannelStatus.INIT && lastRecord.getBatchId() == null)) { + return callChannelAndRecord(input); + } + ChannelPaymentOutput output = new ChannelPaymentOutput(); + output.setSubmitId(lastRecord.getId().toString()); + + //有调用记录 + if (lastRecord.getStatus() == CallChannelStatus.SUCC) { + //调用记录显示已成功支付 + throw new BizException("", "Payment success"); + } else if (lastRecord.getStatus() == CallChannelStatus.PROCESSING && Objects.equals(payTrade.getPayChannel(), lastRecord.getChannel())) { + output.setBatchId(lastRecord.getBatchId()); + output.setStatus(lastRecord.getStatus()); + output.setPaymentUrl(lastRecord.getPaymentUrl()); + return output; + } else { + //调用记录显示未完成 + PayChannelService payChannelService = payChannelRouterService.getPayChannelService(lastRecord.getChannel()); + + ChannelCheckPaymentInput checkPaymentInput = ChannelCheckPaymentInput.builder() + .batchId(lastRecord.getBatchId()) + .transactionId(lastRecord.getTransactionId()) + .tradeNo(payTrade.getTradeNo()) + .build(); + ChannelCheckOutput checkOutput = payChannelService.checkPayment(checkPaymentInput); + if (checkOutput.getStatus() == CallChannelStatus.SUCC) { + //支付渠道返回成功 + updateCheckOutput(lastRecord, checkOutput); + output.setBatchId(lastRecord.getBatchId()); + output.setStatus(lastRecord.getStatus()); + output.setPaymentUrl(lastRecord.getPaymentUrl()); + return output; + } else if (checkOutput.getStatus() == CallChannelStatus.FAIL) { + //支付渠道返回失败 + updateCheckOutput(lastRecord, checkOutput); + + //重新开启一笔支付 + return callChannelAndRecord(input); + } else { + //都不是,取消上一笔 由于这里不退款 先不更新状态。 + payChannelService.cancel(lastRecord.getBatchId()); + updateCheckOutput(lastRecord, checkOutput); + return callChannelAndRecord(input); + } + } + } + + + @Override + public void updateStatus(@NonNull Long id, @NonNull CallChannelStatus status, @NonNull CallChannelStatus expectStatus) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + LambdaUpdateWrapper lambdaUpdateWrapper = updateWrapper.lambda() + .eq(PayCallChannelRecord::getId, id) + .eq(PayCallChannelRecord::getStatus, expectStatus); + PayCallChannelRecord record = PayCallChannelRecord.builder() + .status(status) + .build(); + int n = payCallChannelRecordDao.update(record, lambdaUpdateWrapper); + ToastResultCode.DATA_NOT_EXITS.check(n != 1); + } + + @Override + public Long calculateFee(@NonNull Long amount, @NonNull PayChannel payChannel, @NonNull BizType bizType) { + PayChannelService payChannelService = payChannelRouterService.getPayChannelService(payChannel); + if (bizType == BizType.CHARGE) { + return payChannelService.calculatePaymentFee(amount); + } else if (bizType == BizType.WITHDRAW) { + return payChannelService.calculateWithdrawFee(amount); + } else { + throw new SysException("", "Types of transactions that cannot calculate commissions"); + } + } + + @Override + public Long calculatePlatformFee(Long amount, BizType bizType) { + if (bizType == BizType.WITHDRAW) { + //提现收20%手续费 + return new BigDecimal(amount).multiply(withdrawFeeRate).longValue(); + } else if (bizType == BizType.CHARGE) { + return 0L; + } else { + //其他任何业务场景下都收10%的手续费 + return new BigDecimal(amount).multiply(new BigDecimal(0.1)).longValue(); + } + } + + @Override + public ChannelCreateCustomerOutput callCreateCustomer(@NonNull ChannelCreateCustomerInput input) { + PayChannelService payChannelService = payChannelRouterService.getPayChannelService(input.getPayChannel()); + return payChannelService.createCustomer(input); + } + + @Override + public ChannelCheckOutput checkRecord(PayCallChannelRecord record) { + //查询支付渠道 + BizType bizType = record.getBizType(); + ChannelCheckOutput checkOutput; + PayChannelService payChannelService = payChannelRouterService.getPayChannelService(record.getChannel()); + if (bizType == BizType.GAME || bizType == BizType.CHARGE) { + ChannelCheckPaymentInput input = ChannelCheckPaymentInput.builder() + .batchId(record.getBatchId()) + .transactionId(record.getTransactionId()) + .tradeNo(record.getTradeNo()) + .build(); + //从渠道侧 查询充值结果 + checkOutput = payChannelService.checkPayment(input); + checkOutput.setChannelEvent(TradeEvent.PAYMENT); + } else if (bizType == BizType.WITHDRAW) { + //从渠道侧 查询提现结果 + checkOutput = payChannelService.checkPayout(record.getBatchId()); + checkOutput.setChannelEvent(TradeEvent.WITHDRAW); + } else if (bizType == BizType.REFUND) { + ChannelCheckRefundInput input = ChannelCheckRefundInput.builder() + .batchId(record.getBatchId()) + .build(); + //从渠道侧 查询退款结果 + checkOutput = payChannelService.checkRefund(input); + checkOutput.setChannelEvent(TradeEvent.REFUND); + } else { + throw new BizException("", "Types of transactions that cannot be checked"); + } + //更新 + updateCheckOutput(record, checkOutput); + return checkOutput; + } + + + @Override + public ChannelBindBankCardOutput callBindBankCard(ChannelBindBankCardInput input) { + PayChannelService payChannelService = payChannelRouterService.getPayChannelService(input.getPayChannel()); + return payChannelService.bindBankCard(input); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public void save(PayCallChannelRecord payCallChannelRecord) { + payCallChannelRecordDao.insert(payCallChannelRecord); + } + + /** + * 更新渠道调用记录 + * @param record + * @param checkOutput + */ + private void updateCheckOutput(PayCallChannelRecord record, ChannelCheckOutput checkOutput) { + log.info("channel message,tradeNo {} ,transactionId {},channel status {} ", record.getTradeNo(), checkOutput.getTransactionId(), checkOutput.getStatus()); + LocalDateTime now = LocalDateTime.now(); + record.setTransactionId(checkOutput.getTransactionId()); + record.setStatus(checkOutput.getStatus()); + record.setLastCheckTime(now); + record.setNextCheckTime(now.plusMinutes(5 * (record.getCheckNum() + 1))); + record.setCheckNum(record.getCheckNum() + 1); + record.setResult(checkOutput.getResult()); + + String payerId = ""; + String payerName = ""; + String payerEmail = ""; + try { + payerId = checkOutput.getExtendInfo().get("payerId").toString(); + payerName = checkOutput.getExtendInfo().get("payerName").toString(); + payerEmail = checkOutput.getExtendInfo().get("payerEmail").toString(); + } catch (Exception e) { + //啥也不做 + } + + record.setPayerId(payerId); + record.setPayerName(payerName); + record.setPayerEmail(payerEmail); + + //多渠道支付 只显示了一个渠道的 bill 问题修复。 + if(CallChannelStatus.SUCC.equals(checkOutput.getStatus() )){ + payTradeDao.updateChannelByTradeNo(record.getChannel().name(), record.getTradeNo()); + } + //记录不为最终状态时更新 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .eq(PayCallChannelRecord::getId, record.getId()) + .notIn(PayCallChannelRecord::getStatus, BillStatus.SUCC, BillStatus.FAIL, BillStatus.CANCEL); + int n = payCallChannelRecordDao.update(record, updateWrapper); + + ToastResultCode.IN_PROCESSING.check(n != 1); + if (checkOutput.getStatus() == CallChannelStatus.SUCC) { + //记录渠道账单 + saveChannelBill(record); + } + } + + @Override + public void saveChannelBill(PayCallChannelRecord record) { + InOrOut inOrOut = null; + if (record.getBizType() == BizType.GAME || record.getBizType() == BizType.CHARGE) { + inOrOut = InOrOut.IN; + } else if (record.getBizType() == BizType.WITHDRAW || record.getBizType() == BizType.REFUND) { + inOrOut = InOrOut.OUT; + } + + PayChannelBill payChannelBill = PayChannelBill.builder() + .channelBillId(KeyGenerator.instance().generatorUniqueKey(PayGenrtatorCodeType.CHANNEL_BILL)) + .accountId(SystemUser.SYSTEM_USER_1.getValue()) + .accountName("") + .bizType(record.getBizType()) + .bizNum(record.getTradeNo()) + .status(BillStatus.SUCC.getValue()) + .payChannel(record.getChannel()) + .channelName("") + .channelSn(record.getTransactionId()) + .submitId(record.getId().toString()) + .desAccountNo("") + .desAccountName("") + .inOrOut(inOrOut) + .amount(record.getAmount()) + .build(); + payChannelBillDao.insert(payChannelBill); + } + + private ChannelPaymentOutput callChannelAndRecord(ChannelPaymentInput input) { + LocalDateTime callChannelRecordCreateTime = LocalDateTime.now(); + //默认过期时间30分钟 + LocalDateTime expTime = callChannelRecordCreateTime.plusMinutes(30); + PayCallChannelRecord record = PayCallChannelRecord.builder() + .tradeNo(input.getPayTrade().getTradeNo()) + .bizType(input.getPayTrade().getBizType()) + .channel(input.getPayTrade().getPayChannel()) + .status(CallChannelStatus.INIT) + .amount(input.getPayTrade().getOccurAmount()) + .exchangeAmount(input.getPayTrade().getExchangeOccurAmount()) + .exchangeCurrency(input.getPayTrade().getExchangeOccurCurrency()) + .nextCheckTime(callChannelRecordCreateTime) + .createTime(callChannelRecordCreateTime) + //根据渠道设置过期时间 + .expTime(expTime) + .id(IdWorker.getId()) + .build(); + payCallChannelRecordDao.insert(record); + + if(BizType.CHARGE.equals(input.getPayTrade().getBizType())){ + //创建渠道调用记录的时候同时创建一条待 处理记录 + try { + //默认的下次执行时间为当前时间 + LocalDateTime nextHandTime = callChannelRecordCreateTime; + ProcessingCharge processingCharge = ProcessingCharge.builder() + .tradeNo(input.getPayTrade().getTradeNo()) + .callChannelRecordId(record.getId()) + .payChannel(input.getPayTrade().getPayChannel()) + .createTime(callChannelRecordCreateTime) + //根据渠道来判断设置下次的执行时间 + .nextHandTime(nextHandTime) + .handCount(0) + .build(); + processingChargeDao.insert(processingCharge); + } catch (DuplicateKeyException e) { + //当触发唯一索引时不做任何处理,以保证单个交易号在不同渠道中永远只会存在一条数据 + ToastResultCode.PARAM_ERROR.check(true, "", "The transaction exists. DO NOT make duplicate payments"); + } + } + + + input.setSubmitId(String.valueOf(record.getId())); + PayChannelService payChannelService = payChannelRouterService.getPayChannelService(input.getPayTrade().getPayChannel()); + ChannelPaymentOutput output = payChannelService.payment(input); + //将submitId返回给前端 + output.setSubmitId(input.getSubmitId()); + + String payerId = ""; + String payerName = ""; + String payerEmail = ""; + if (output.getExtendInfo() != null) { + payerId = output.getExtendInfo().get("payerId") == null ? "" : output.getExtendInfo().get("payerId").toString(); + payerName = output.getExtendInfo().get("payerName") == null ? "" : output.getExtendInfo().get("payerName").toString(); + payerEmail = output.getExtendInfo().get("payerEmail") == null ? "" : output.getExtendInfo().get("payerEmail").toString(); + } + PayCallChannelRecord updater = PayCallChannelRecord.builder() + .id(record.getId()) + .status(output.getStatus()) + .batchId(output.getBatchId()) + .transactionId(output.getTransactionId()) + .paymentUrl(output.getPaymentUrl()) + .payerId(payerId) + .payerEmail(payerEmail) + .payerName(payerName) + .result(output.getResult()) + .build(); + payCallChannelRecordDao.updateById(updater); + return output; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayChannelRouterServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayChannelRouterServiceImpl.java new file mode 100644 index 0000000..16f95eb --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayChannelRouterServiceImpl.java @@ -0,0 +1,39 @@ +package com.sonic.lion.service.impl; + +import com.sonic.common.exception.SysException; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.service.PayChannelRouterService; +import com.sonic.lion.service.PayChannelService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +import static com.sonic.lion.enums.PayChannel.STRIPE; + + +/** + * @author: code + * @date: 2025/06/01 + * @Description: + * @version: 1.0.0 + */ +@Service +public class PayChannelRouterServiceImpl implements PayChannelRouterService { + + @Autowired + @Qualifier("stripeService") + private PayChannelService stripeService; + + + @Override + public PayChannelService getPayChannelService(@NonNull PayChannel payChannel) { + if (payChannel == STRIPE) { + return stripeService; + } else { + //不支持的支付渠道 + throw new SysException("", ""); + } + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayChargeServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayChargeServiceImpl.java new file mode 100644 index 0000000..4e4b71c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayChargeServiceImpl.java @@ -0,0 +1,124 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.dao.AppStoreProductDao; +import com.sonic.lion.dao.PayChargeDao; +import com.sonic.lion.domain.entity.AppStoreProduct; +import com.sonic.lion.domain.entity.PayCharge; +import com.sonic.lion.domain.enums.ProductType; +import com.sonic.lion.domain.input.SubChargeProductListInput; +import com.sonic.lion.domain.output.SubProductListOutput; +import com.sonic.lion.enums.MemberType; +import com.sonic.lion.service.PayChargeService; +import com.sonic.lion.utils.BeanConvert; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author: code + * @date: 2025/06/24 + * @Description: + * @version: 1.0.0 + */ +@Service +public class PayChargeServiceImpl implements PayChargeService { + + @Autowired + private PayChargeDao payChargeDao; + + @Autowired + private AppStoreProductDao productDao; + + @Override + public PayCharge getByBundleIdAndProductIdAndPlatform(String bundleId, String productId, String platform) { + if (StringUtils.isBlank(bundleId) || StringUtils.isBlank(productId)) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(PayCharge::getBundleId, bundleId) + .eq(PayCharge::getProductId, productId) + .eq(PayCharge::getPlatform, platform); + return payChargeDao.selectOne(queryWrapper); + } + + @Override + public PayCharge getByBundleIdAndProductIdAndPlatform(String productId, String platform) { + if (StringUtils.isBlank(productId)) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(PayCharge::getProductId, productId) + .eq(PayCharge::getPlatform, platform); + return payChargeDao.selectOne(queryWrapper); + } + + @Override + public List list(String platform) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(PayCharge::getPlatform, platform); + queryWrapper = queryWrapper.eq(PayCharge::getDeleted, false).orderByAsc(PayCharge::getChargeAmount); + return payChargeDao.selectList(queryWrapper); + } + + @Override + public List list(String platform, String version) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(PayCharge::getPlatform, platform) + .eq(PayCharge::getVersion, version) + .eq(PayCharge::getDeleted, false).orderByAsc(PayCharge::getChargeAmount); + return payChargeDao.selectList(queryWrapper); + } + + @Override + public List subProductList(SubChargeProductListInput input) { + List vipList = listSubscriptionProducts(input.getPlatform(), input.getVersion(), MemberType.VIP); + return BeanConvert.copeList(vipList, SubProductListOutput.class); + } + + /** + * 获取订阅产品 + * @param platform + * @param version + * @param memberType + * @return + */ + private List listSubscriptionProducts(String platform, String version , MemberType memberType) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(AppStoreProduct::getPlatform, platform) + .eq(AppStoreProduct::getVersion, version) + .eq(AppStoreProduct::getProductType, ProductType.SUBSCRIPTION) + .eq(AppStoreProduct::getMemberType, memberType) + .eq(AppStoreProduct::getDeleted, false).orderByAsc(AppStoreProduct::getChargeAmount); + return productDao.selectList(queryWrapper); + } + + @Override + public AppStoreProduct getAppStoreProduct(String productId, String platform) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(AppStoreProduct::getDeleted, false) + .eq(AppStoreProduct::getProductId, productId) + .eq(AppStoreProduct::getPlatform, platform); + return productDao.selectOne(queryWrapper); + } + + + @Override + public PayCharge getByProductId(String productId) { + return getByProductId(productId, null); + } + + @Override + public PayCharge getByProductId(String productId, String platform) { + if (StringUtils.isBlank(productId)) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(PayCharge::getProductId, productId) + .eq(StringUtils.isNotEmpty(platform), PayCharge::getPlatform, platform); + return payChargeDao.selectOne(queryWrapper); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayConfigServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayConfigServiceImpl.java new file mode 100644 index 0000000..4abc75b --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayConfigServiceImpl.java @@ -0,0 +1,143 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Lists; +import com.sonic.lion.dao.PayConfigDao; +import com.sonic.lion.domain.entity.PayConfig; +import com.sonic.lion.domain.output.EnabledPayChannelOutput; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.service.PayConfigService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 支付配置 + */ +@Service +public class PayConfigServiceImpl implements PayConfigService { + + @Autowired + private PayConfigDao payConfigDao; + + @Override + public String getByKey(String key) { + if (StringUtils.isBlank(key)) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(PayConfig::getKey, key); + return payConfigDao.selectOne(queryWrapper).getValue(); + } + + @Override + public String getGoogleFeeRatio(){ + return getByKey("googleFeeRatio"); + } + + @Override + public String getIOSFeeRatio() { + return getByKey("iosFeeRatio"); + } + + @Override + public List getEnabledPayChannel() { + return Lists.newArrayList(getByKey("enabledPayChannel").split(",")); + } + + @Override + public List getAllPayChannelDebug() { + return Lists.newArrayList(PayChannel.BUFF.name(), PayChannel.STRIPE.name()); + } + + @Override + public List getEnabledPayOutChannel() { + if(StringUtils.isBlank(getByKey("getEnabledPayOutChannel"))){ + return new ArrayList<>(); + } + return Arrays.asList(getByKey("getEnabledPayOutChannel").split(",")); + } + + @Override + public boolean paymentChannelSwitchCheckPass(Long currentUserId, PayChannel payChannel) { + List configList = getEnabledPayChannel(); + //当前发起提现的渠道在配置中 + if(!CollectionUtils.isEmpty(configList) && configList.contains(payChannel.name())) { + return true; + } + //判断当前访问用户是否是特殊测试账号 + List debugUserIds = getDebugPayUserIds(); + if(!CollectionUtils.isEmpty(debugUserIds) && debugUserIds.contains(currentUserId.toString())) { + return true; + } + return false; + } + + @Override + public boolean payoutChannelSwitchCheckPass(Long currentUserId, PayChannel payChannel) { + List configList = getEnabledPayOutChannel(); + //当前发起提现的渠道在配置中 + if(!CollectionUtils.isEmpty(configList) && configList.contains(payChannel.name())) { + return true; + } + //判断当前访问用户是否是特殊测试账号 + List debugUserIds = getDebugPayUserIds(); + if(!CollectionUtils.isEmpty(debugUserIds) && debugUserIds.contains(currentUserId.toString())) { + return true; + } + return false; + } + + @Override + public List getDebugPayUserIds() { + return Arrays.asList(getByKey("debugPayUserIds").split(",")); + } + + @Override + public boolean enableFirstOrderReward() { + return Boolean.parseBoolean(getByKey("enableFirstOrderReward")); + } + + + @Override + public boolean enableEveryFridayReward() { + return Boolean.parseBoolean(getByKey("enableEveryFridayReward")); + } + + @Override + public boolean enableWithdrawFeeReduction() { + return Boolean.parseBoolean(getByKey("enableWithdrawFeeReduction")); + } + + @Override + public LocalDateTime withdrawFeeReductionEndTime() { + String withdrawFeeReductionEndTime = getByKey("withdrawFeeReductionEndTime"); + if (StringUtils.isNotEmpty(withdrawFeeReductionEndTime)) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return LocalDateTime.parse(withdrawFeeReductionEndTime, formatter); + } + return null; + } + + @Override + public EnabledPayChannelOutput enabledPayChannel(Long userId) { + List debugUserIds = getDebugPayUserIds(); + List channels; + if (debugUserIds != null && debugUserIds.contains(userId.toString())) { + channels = getAllPayChannelDebug(); + } else { + channels = getEnabledPayChannel(); + } + EnabledPayChannelOutput output = new EnabledPayChannelOutput(); + output.setChannels(channels); + return output; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayTradeServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayTradeServiceImpl.java new file mode 100644 index 0000000..8552f4d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PayTradeServiceImpl.java @@ -0,0 +1,803 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.common.AppRuntime; +import com.sonic.common.exception.SysException; +import com.sonic.common.utils.DateConvertUtils; +import com.sonic.lion.dao.AccountBuffDao; +import com.sonic.lion.dao.PayTradeDao; +import com.sonic.lion.dao.WithdrawRequestDao; +import com.sonic.lion.domain.bo.TradeStatusBo; +import com.sonic.lion.domain.entity.BuffRewardRecord; +import com.sonic.lion.domain.entity.PayCharge; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.enums.*; +import com.sonic.lion.domain.input.*; +import com.sonic.lion.domain.output.ChannelPaymentOutput; +import com.sonic.lion.domain.output.PrePaymentOutput; +import com.sonic.lion.domain.output.TradeHandleOutput; +import com.sonic.lion.domain.req.RefundReq; +import com.sonic.lion.domain.resp.CheckoutResp; +import com.sonic.lion.enums.*; +import com.sonic.lion.service.*; +import com.sonic.lion.utils.KeyGenerator; +import com.sonic.lion.utils.TransactionUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static com.sonic.lion.enums.PayGenrtatorCodeType.TRADE; + +/** + * @author: code + * @date: 2025/05/11 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@Service +public class PayTradeServiceImpl implements PayTradeService { + + @Value("${site.type}") + private String siteType; + @Autowired + private AppRuntime appRuntime; + @Autowired + private AccountBuffService accountBuffService; + @Autowired + private AccountBuffDao accountBuffDao; + @Autowired + private PayTradeDao payTradeDao; + @Autowired + private PayCallChannelService payCallChannelService; + @Autowired + private ActivityService activityService; + @Autowired + private WithdrawRequestDao withdrawRequestDao; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private CheckOutService checkOutService; + + + @Override + public PayTrade getByTradeNo(String tradeNo) { + if (StringUtils.isBlank(tradeNo)) { + return null; + } + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery() + .eq(PayTrade::getTradeNo, tradeNo); + return payTradeDao.selectOne(lambdaQueryWrapper); + } + + + /** + * 支付预下单 初始状态 等待支付 + * + * @param input + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public PrePaymentOutput prePayment(@NonNull PrePaymentInput input) { + validPrePaymentInput(input); + + String tradeNo = KeyGenerator.instance().generatorUniqueKey(TRADE); + String outTradeNo = input.getOutTradeNo(); + if (StringUtils.isNotBlank(outTradeNo)) { + ToastResultCode.DUPLICATE_ORDER_NUMBER.check(getByPlatformAndOuterTradeNo(input.getPlatform(), outTradeNo) != null); + } else { + outTradeNo = tradeNo; + } + + PayTrade payTrade = new PayTrade(); + payTrade.setIp(input.getIp()); + payTrade.setProductId(input.getProductId()); + payTrade.setPlatform(input.getPlatform()); + payTrade.setOutTradeNo(outTradeNo); + payTrade.setOutTradeNoRelationNo(input.getOutTradeNoRelationNo()); + payTrade.setTradeNo(tradeNo); + payTrade.setSrcAccountId(input.getSrcAccountId()); + payTrade.setSrcAccountName(input.getSrcAccountName()); + payTrade.setDesAccountNo(input.getDesAccountNo()); + payTrade.setDesAccountName(input.getDesAccountName()); + payTrade.setBizType(input.getBizType()); + payTrade.setName(input.getName()); + payTrade.setAmount(input.getProductAmount()); + payTrade.setActualAmount(input.getActualAmount()); + payTrade.setPlatformFee(input.getPlatformFee() == null ? 0L : input.getPlatformFee()); + + payTrade.setThirdFee(input.getThirdFee() == null ? 0L : input.getThirdFee()); + payTrade.setFee(input.getFee() == null ? 0L : input.getFee()); + payTrade.setPromoAmount(input.getPromoAmount() == null ? 0L : input.getPromoAmount()); + payTrade.setOccurAmount(input.getOccurAmount() == null ? 0L : input.getOccurAmount()); + payTrade.setPaymentTradeNo(input.getPaymentTradeNo()); + payTrade.setStatus(TradeStatus.WAITPAY); + payTrade.setStatusInTime(LocalDateTime.now()); + payTrade.setCloseTime(input.getCloseTime()); + payTrade.setResourceKey(input.getResourceKey()); + payTrade.setResourceNum(input.getResourceNum()); + payTrade.setExtend(input.getExtend()); + payTrade.setClientVersion(input.getClientVersion()); + if (input.getPayChannel() != null) { + payTrade.setPayChannel(input.getPayChannel()); + } + if (input.getPaymentType() != null) { + payTrade.setPaymentType(input.getPaymentType()); + } + payTrade.setCreateTime(LocalDateTime.now()); + + + //充值成功 获取赠送金额 + PayCharge payCharge = activityService.getRewardTypeByUserIdV2(payTrade.getSrcAccountId(), payTrade.getCreateTime(), payTrade.getProductId(), payTrade.getClientVersion()); + if (payCharge != null) { + //如果有赠送金额 就要加上赠送金额 + payTrade.setGiftAmount(payCharge.getGiftAmount()); + } + + payTradeDao.insert(payTrade); + + return PrePaymentOutput.builder() + .tradeNo(payTrade.getTradeNo()) + .outTradeNo(payTrade.getOutTradeNo()) + .outTradeNoRelationNo(payTrade.getOutTradeNoRelationNo()) + .tradeStatus(TradeStatus.WAITPAY) + .build(); + } + + @Transactional + @Override + public CheckoutResp checkout(@NonNull CheckoutInput input) { + PayTrade payTrade = getByTradeNo(input.getTradeNo()); + ToastResultCode.DATA_NOT_EXITS.check(payTrade == null); + TradeStatus tradeStatus = payTrade.getStatus(); + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(tradeStatus != TradeStatus.WAITPAY); + ToastResultCode.PAY_TRADE_PAYER_ERROR.check(!Objects.equals(input.getUid(), payTrade.getSrcAccountId())); + + BizType bizType = payTrade.getBizType(); + TradeType tradeType = bizType.getTradeType(); + ToastResultCode.PAY_TRADE_PAYMENT_TYPE_ERROR.check(tradeType == TradeType.CHARGE && input.getPaymentType() == PaymentType.BALANCE); + + long fee = 0; + CheckoutResp resp = new CheckoutResp(); + if (input.getPaymentType() == PaymentType.BALANCE) { + TradeStatusBo tradeStatusBo = checkOutByBalance(input, payTrade, bizType, tradeType, fee); + tradeStatus = tradeStatusBo.getTradeStatus(); + //将流水ID列表设置到出参对象中 + resp.setBl(tradeStatusBo.getBillIdList()); + } else if (input.getPaymentType() == PaymentType.CHANNEL) { + fee = payCallChannelService.calculateFee(payTrade.getAmount(), input.getPayChannel(), bizType); + Long platformFee = payCallChannelService.calculatePlatformFee(payTrade.getAmount(), bizType); + payTrade.setPlatformFee(platformFee); + payTrade.setThirdFee(fee - payTrade.getPlatformFee()); + + updatePaymentInfo(input, payTrade, fee); + + //调用支付渠道 + ChannelPaymentInput channelPaymentInput = ChannelPaymentInput.builder() + .payTrade(payTrade) + .returnUrl(input.getReturnUrl()) + .cancelUrl(input.getCancelUrl()) + .ip(payTrade.getIp()) + .build(); + ChannelPaymentOutput output = payCallChannelService.callPayment(channelPaymentInput); + ToastResultCode.DATA_STATUS_CHANGED.check(output.getStatus() == CallChannelStatus.FAIL); + if (output.getStatus() == CallChannelStatus.SUCC) { + tradeStatus = TradeStatus.PAID; + } + resp.setSubmitId(output.getSubmitId()); + resp.setPaymentUrl(output.getPaymentUrl()); + } + if (tradeStatus == TradeStatus.PAID) { + //付款成功后处理 + getProxy().tradeHandle(payTrade, TradeEvent.PAYMENT); + } + resp.setTradeStatus(tradeStatus); + resp.setFee(fee); + return resp; + } + + + /** + * 使用余额 付款 + * + * @param input + * @param payTrade + * @param bizType + * @param tradeType + * @param fee + * @return + */ + private TradeStatusBo checkOutByBalance(CheckoutInput input, PayTrade payTrade, BizType bizType, TradeType tradeType, long fee) { + //声明返回对象 + TradeStatusBo tradeStatusBo = new TradeStatusBo(); + + TradeStatus tradeStatus; + updatePaymentInfo(input, payTrade, fee); + + //扣除付款方余额 + BuffTransferInput buffTransferInput = BuffTransferInput.builder() + .payChannel(input.getPayChannel()) + .platform(payTrade.getPlatform()) + .decUid(payTrade.getSrcAccountId()) + .decBuffType(BuffType.BALANCE) + .decBuff(payTrade.getOccurAmount()) + .decTargetUserId(Long.valueOf(payTrade.getDesAccountNo())) + .tradeNo(payTrade.getTradeNo()) + .bizNo(payTrade.getOutTradeNo()) + .bizNoRelationNo(payTrade.getOutTradeNoRelationNo()) + .bizType(bizType) + .extend(payTrade.getExtend()) + .build(); + if (tradeType == TradeType.SECURED_TRANSACTION || tradeType == TradeType.C2B) { + //转到系统余额 + buffTransferInput.setAddUid(SystemUser.SYSTEM_USER_1.getValue()); + buffTransferInput.setAddBuffType(BuffType.BALANCE); + buffTransferInput.setAddBuff(payTrade.getOccurAmount()); + } else if (tradeType == TradeType.NON_SECURED_TRANSACTION) { + //转到收款方待入账 + buffTransferInput.setAddUid(Long.valueOf(payTrade.getDesAccountNo())); + buffTransferInput.setAddBuffType(BuffType.AWAITING_INCOME); + buffTransferInput.setAddBuff(payTrade.getAmount()); + } + //根据业务类型判断,给用户和系统账户 加钱 + if (bizType == BizType.GIFT || bizType == BizType.IMAGE_UNLOCK) { + //转到收款方待入账 + buffTransferInput.setAddUid(Long.valueOf(payTrade.getDesAccountNo())); + buffTransferInput.setAddBuffType(BuffType.AWAITING_INCOME); + buffTransferInput.setAddBuff(payTrade.getAmount() - payTrade.getPlatformFee()); + + //转到系统账号的收款方待入账 + buffTransferInput.setAddSystemUid(SystemUser.SYSTEM_USER_2.getValue()); + buffTransferInput.setAddSystemBuffType(BuffType.AWAITING_INCOME); + buffTransferInput.setAddSystemBuff(payTrade.getPlatformFee()); + } + List billIdList = accountBuffService.transfer(buffTransferInput); + //将流水id列表设置到返回对象中 + tradeStatusBo.setBillIdList(billIdList); + tradeStatus = TradeStatus.PAID; + tradeStatusBo.setTradeStatus(tradeStatus); + return tradeStatusBo; + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public TradeHandleOutput tradeHandleV2(PayChannel payChannel, @NonNull String tradeNo, @NonNull TradeEvent tradeEvent) { + PayTrade payTrade = getByTradeNo(tradeNo); + payTrade.setPayChannel(payChannel); + ToastResultCode.DATA_NOT_EXITS.check(payTrade == null); + return tradeHandle(payTrade, tradeEvent); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void refund(@NonNull RefundReq req) { + ToastResultCode.OUT_TRADE_NO_EMPTY.check(CollectionUtils.isEmpty(req.getOutTradeNoList())); + for (String outTradeNo : req.getOutTradeNoList()) { + PayTrade payTrade = getByPlatformAndOuterTradeNo(req.getPlatform(), outTradeNo); + ToastResultCode.DATA_NOT_EXITS.check(payTrade == null); + ToastResultCode.UNSUPPORTED_BUSINESS_TYPE.check(payTrade.getBizType() != BizType.GAME); + + PaymentType paymentType = payTrade.getPaymentType(); + TradeStatus status = payTrade.getStatus(); + BuffTransferInput buffTransferInput = new BuffTransferInput(); + if (paymentType == PaymentType.BALANCE) { + if (status == TradeStatus.PAID) { + //已付款状态退款,此时钱还在系统账户 + //将系统余额转入用户余额 + buffTransferInput = BuffTransferInput.builder() + .platform(payTrade.getPlatform()) + .decUid(SystemUser.SYSTEM_USER_1.getValue()) + .decBuffType(BuffType.BALANCE) + .decBuff(payTrade.getOccurAmount()) + .addUid(payTrade.getSrcAccountId()) + .addBuffType(BuffType.BALANCE) + .addBuff(payTrade.getOccurAmount()) + .addTargetUserId(Long.valueOf(payTrade.getDesAccountNo())) + .tradeNo(payTrade.getTradeNo()) + .bizNoRelationNo(payTrade.getOutTradeNoRelationNo()) + .bizNo(payTrade.getOutTradeNo()) + .bizType(BizType.REFUND) + .build(); + } else if (status == TradeStatus.FINISHED) { + //已完成状态退款,此时钱已到收款方账户 + //收款方待入账金额转入付款方余额 + buffTransferInput = BuffTransferInput.builder() + .platform(payTrade.getPlatform()) + .decUid(Long.valueOf(payTrade.getDesAccountNo())) + .decBuffType(BuffType.AWAITING_INCOME) + .decBuff(payTrade.getAmount()) + .decTargetUserId(payTrade.getSrcAccountId()) + .addUid(payTrade.getSrcAccountId()) + .addBuffType(BuffType.BALANCE) + .addBuff(payTrade.getOccurAmount()) + .addTargetUserId(Long.valueOf(payTrade.getDesAccountNo())) + .tradeNo(payTrade.getTradeNo()) + .bizNoRelationNo(payTrade.getOutTradeNoRelationNo()) + .bizNo(payTrade.getOutTradeNo()) + .bizType(BizType.REFUND) + .fullRefund(true) + .build(); + + } else { + ToastResultCode.DATA_STATUS_CHANGED.check(true); + } + accountBuffService.transfer(buffTransferInput); + //更新交易状态 + updateStatus(payTrade.getId(), TradeStatus.REFUNDED, status); + //buff退款不用调用第三方系统, 可以直接完成到已退款状态同步返回退款结果 + } else { + ToastResultCode.UNSUPPORTED_BUSINESS_TYPE.check(true); + } + } + } + + @Override + public PayTrade getByPlatformAndOuterTradeNo(String platform, String outTradeNo) { + if (StringUtils.isBlank(platform) || StringUtils.isBlank(outTradeNo)) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(PayTrade::getPlatform, platform) + .eq(PayTrade::getOutTradeNo, outTradeNo); + PayTrade payTrade = payTradeDao.selectOne(queryWrapper); + //为兼容老订单新支付,如果找不到getPlatform换成1找找看,下个版本可删除 + if (payTrade == null) { + LambdaQueryWrapper queryWrapper2 = Wrappers.lambdaQuery() + .eq(PayTrade::getPlatform, "1") + .eq(PayTrade::getOutTradeNo, outTradeNo); + return payTradeDao.selectOne(queryWrapper2); + } + return payTrade; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void completeSecuredTrade(@NonNull String platform, @NonNull String outTradeNo) { + + PayTrade payTrade = getByPlatformAndOuterTradeNo(platform, outTradeNo); + ToastResultCode.DATA_NOT_EXITS.check(payTrade == null); + //交易已完成 + if (payTrade.getComplete()) { + return; + } + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(payTrade.getStatus() != TradeStatus.PAID); + + updateStatus(payTrade.getId(), TradeStatus.FINISHED, TradeStatus.PAID); + + //将系统余额转入收款方待入账金额 + BuffTransferInput buffTransferInput = BuffTransferInput.builder() + .platform(payTrade.getPlatform()) + .decUid(SystemUser.SYSTEM_USER_1.getValue()) + .decBuffType(BuffType.BALANCE) + .decBuff(payTrade.getAmount()) + .addUid(Long.valueOf(payTrade.getDesAccountNo())) + .addBuffType(BuffType.AWAITING_INCOME) + .addBuff(payTrade.getAmount()) + .addTargetUserId(payTrade.getSrcAccountId()) + .tradeNo(payTrade.getTradeNo()) + .bizNo(payTrade.getOutTradeNo()) + .bizType(payTrade.getBizType()) + .extend(payTrade.getExtend()) + .build(); + accountBuffService.transfer(buffTransferInput); + } + + + @Transactional + @Override + public void closeTrade(@NonNull String platform, @NonNull String outTradeNo) { + PayTrade payTrade = getByPlatformAndOuterTradeNo(platform, outTradeNo); + if (payTrade == null) { + log.warn("不能找到需要关闭的交易, platform: {}, outTradeNo: {}", platform, outTradeNo); + return; + } + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(payTrade.getStatus() != TradeStatus.WAITPAY); + updateStatus(payTrade.getId(), TradeStatus.CLOSED, TradeStatus.WAITPAY); + //todo 关闭支付渠道交易 + } + + @Transactional + @Override + public int closeWaitPayTrade() { + return payTradeDao.closeWaitPayTrade(); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public TradeHandleOutput tradeHandle(@NonNull PayTrade payTrade, @NonNull TradeEvent tradeEvent) { + //所有交易上锁60秒 + String lockKey = appRuntime.buildPrefixKey("lock", "trade", payTrade.getTradeNo()); + boolean onLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock", 60L, TimeUnit.SECONDS); + ToastResultCode.IN_PROCESSING.check(!onLock); + log.info("payTrade:{}", payTrade); + BizType bizType = payTrade.getBizType(); + TradeType tradeType = bizType.getTradeType(); + TradeStatus status = null; + if (tradeEvent == TradeEvent.PAYMENT) { + status = paymentEventHandler(payTrade, tradeType); + } else if (tradeEvent == TradeEvent.WITHDRAW) { + // + } else if (tradeEvent == TradeEvent.REFUND) { + status = refundEventHandler(payTrade, tradeType); + } + TradeHandleOutput output = new TradeHandleOutput(); + output.setUid(payTrade.getSrcAccountId()); + output.setTradeNo(payTrade.getTradeNo()); + output.setOutTradeNo(payTrade.getOutTradeNo()); + output.setAmount(payTrade.getAmount()); + output.setBizType(bizType); + output.setStatusInTime(LocalDateTime.now()); + output.setPaymentType(payTrade.getPaymentType()); + output.setStatus(status); + output.setFee(payTrade.getFee()); + output.setPromoAmount(payTrade.getPromoAmount()); + output.setOccurAmount(payTrade.getOccurAmount()); + stringRedisTemplate.delete(lockKey); + return output; + } + + /** + * 退款事件处理 + * + * @param payTrade + * @param tradeType + * @return + */ + private TradeStatus refundEventHandler(@NonNull PayTrade payTrade, TradeType tradeType) { + if (tradeType == TradeType.SECURED_TRANSACTION) { + return refundAfterHandle(payTrade); + } else { + throw new SysException("", "暂不支持该交易类型的退款事件处理"); + } + } + + /** + * 付款事件处理 + * + * @param payTrade + * @param tradeType + * @return + */ + private TradeStatus paymentEventHandler(@NonNull PayTrade payTrade, TradeType tradeType) { + if (tradeType == TradeType.SECURED_TRANSACTION) { + return securedPaymentAfterHandle(payTrade); + } else if (tradeType == TradeType.NON_SECURED_TRANSACTION) { + return nonSecuredPaymentAfterHandle(payTrade); + } else if (tradeType == TradeType.CHARGE) { + return chargePaymentAfterHandle(payTrade); + } else if (tradeType == TradeType.C2B) { + return c2bPaymentAfterHandle(payTrade); + } else { + throw new SysException("", "暂不支持该交易类型的支付事件处理"); + } + } + + /** + * 担保交易渠道处理成功后系统内处理 + * + * @param payTrade + */ + private TradeStatus securedPaymentAfterHandle(PayTrade payTrade) { + updateStatus(payTrade.getId(), TradeStatus.PAID, TradeStatus.WAITPAY); + return TradeStatus.PAID; + } + + /** + * 非担保交易渠道处理成功后系统内处理 + * + * @param payTrade + */ + private TradeStatus nonSecuredPaymentAfterHandle(PayTrade payTrade) { + updateStatus(payTrade.getId(), TradeStatus.FINISHED, TradeStatus.WAITPAY); + return TradeStatus.FINISHED; + } + + /** + * c2b交易支付后处理 + * + * @param payTrade + */ + private TradeStatus c2bPaymentAfterHandle(PayTrade payTrade) { + updateStatus(payTrade.getId(), TradeStatus.FINISHED, TradeStatus.WAITPAY); + return TradeStatus.FINISHED; + } + + /** + * 渠道退款处理成功后系统内处理 + * + * @param payTrade + */ + private TradeStatus refundAfterHandle(PayTrade payTrade) { + updateStatus(payTrade.getId(), TradeStatus.REFUNDED, TradeStatus.REFUNDING); + return TradeStatus.REFUNDED; + } + + /** + * 渠道支付成功后系统内充值处理 + * + * @param payTrade + */ + private TradeStatus chargePaymentAfterHandle(PayTrade payTrade) { + //查询数据库的 paytrade 看状态是FINISHED 说明是其他渠道 再次充值。 不能再次 扣款 + PayTrade payTradeDb = payTradeDao.selectById(payTrade.getId()); + boolean isFinished = TradeStatus.FINISHED.equals(payTradeDb.getStatus()); + + //将系统余额转入收款方待入账金额 + BuffTransferInput buffTransferInput = BuffTransferInput.builder() + .platform(payTrade.getPlatform()) + + .addUid(payTrade.getSrcAccountId()) + .addBuffType(BuffType.BALANCE) + //添加实得金额 + .addBuff(payTrade.getActualAmount()) + .payChannel(payTrade.getPayChannel()) + .tradeNo(payTrade.getTradeNo()) + .bizNo(payTrade.getOutTradeNo()) + .bizType(payTrade.getBizType()) + .payMethod(payTrade.getPayMethod()) + .build(); + + //充值成功 获取赠送金额 + //获取充值档位的赠送基础数据 + PayCharge payCharge = activityService.getRewardTypeByUserIdV2(payTrade.getSrcAccountId(), payTrade.getCreateTime(), payTrade.getProductId(), payTrade.getClientVersion()); + if (payCharge != null) { + //如果有赠送金额 就要加上赠送金额 + buffTransferInput.setGiftAmount(payCharge.getGiftAmount()); + buffTransferInput.setAddBuff(payTrade.getActualAmount() + buffTransferInput.getGiftAmount()); + + payTradeDao.updateGiftAmountByTradeNo(payTrade.getId(), buffTransferInput.getGiftAmount()); + } else { + payTradeDao.updateGiftAmountByTradeNo(payTrade.getId(), 0L); + } + + //执行转账操作 + accountBuffService.transfer(buffTransferInput); + + if (payCharge != null) { + //TODO 对这个表的数据加监控?监控是否会多送 + Integer yearMonthInt = com.sonic.lion.utils.DateConvertUtils.formatYearMonthToInt(DateConvertUtils.toZone(LocalDateTime.now(), "Asia/Shanghai", "America/Los_Angeles")); + BuffRewardRecord buffRewardRecord = BuffRewardRecord.builder() + .productId(payTrade.getProductId()) + .rewardDate(DateConvertUtils.toZone(payTrade.getCreateTime(), "Asia/Shanghai", "America/Los_Angeles").toLocalDate()) + .amount(buffTransferInput.getGiftAmount()) + .uid(payTrade.getSrcAccountId()).createTime(payTrade.getCreateTime()) + .yearMonthInt(yearMonthInt) + .rewardType(payCharge.getBizType()).build(); + activityService.saveRecord(buffRewardRecord); + } + + //充值且支付逻辑 第二笔不能再扣款 + if (StringUtils.isNotBlank(payTrade.getPaymentTradeNo()) && !isFinished) { + TransactionUtils.afterCommitSyncExecute(() -> { + checkOutService.chargeAfterPayment(payTrade.getPaymentTradeNo(), payTrade.getSrcAccountId()); + }); + } + updateStatus(payTrade.getId(), TradeStatus.FINISHED); + return TradeStatus.FINISHED; + } + + /** + * 更新交易状态 + * + * @param id payTrade主键 + * @param status 更新后状态 + * @param expectStatus 期望现在的状态 + * @return + */ + @Transactional + @Override + public void updateStatus(Long id, TradeStatus status, TradeStatus expectStatus) { + LocalDateTime now = LocalDateTime.now(); + + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .eq(PayTrade::getId, id); + + PayTrade updater = PayTrade.builder() + .status(status) + .statusInTime(now) + .build(); + + if (expectStatus != null) { + updateWrapper.eq(PayTrade::getStatus, expectStatus); + } + + if (status == TradeStatus.PAID) { + updater.setPayTime(now); + } else if (status == TradeStatus.FINISHED) { + if (expectStatus == TradeStatus.WAITPAY) { + //直接由待支付状态转到完成状态,更新支付时间 + updater.setPayTime(now); + } + updater.setComplete(true); + updater.setFinishTime(now); + } + + int n = payTradeDao.update(updater, updateWrapper); + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(n != 1); + } + + + private void updateStatus(Long id, TradeStatus status) { + LocalDateTime now = LocalDateTime.now(); + + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .eq(PayTrade::getId, id); + + PayTrade updater = PayTrade.builder() + .status(status) + .statusInTime(now) + .build(); + + if (status == TradeStatus.PAID) { + updater.setPayTime(now); + } else if (status == TradeStatus.FINISHED) { + updater.setComplete(true); + updater.setFinishTime(now); + } + int n = payTradeDao.update(updater, updateWrapper); + } + + /** + * 更新交易单支付信息 + * + * @param input + * @param payTrade + * @param fee + */ + @Override + @Transactional + public void updatePaymentInfo(CheckoutInput input, PayTrade payTrade, long fee) { + long occurAmount = payTrade.getAmount() + fee - payTrade.getPromoAmount(); + //TODO by code 临时需求(使用优惠码进行免单由平台付款给卖家) 即 校验bizType如果是Game的订单,支付金额是0元的话 则不作数据校验 + if (input.getPaymentType() == PaymentType.BALANCE && BizType.GAME != payTrade.getBizType()) { + validOccurAmount(occurAmount); + } + //小于0判断,如果小于0则给 默认值为0 + occurAmount = occurAmount <= 0 ? 0 : occurAmount; + payTrade.setPaymentType(input.getPaymentType()); + payTrade.setFee(fee); + payTrade.setOccurAmount(occurAmount); + payTrade.setPayChannel(input.getPayChannel()); + payTradeDao.updateById(payTrade); + } + + private void validOccurAmount(long occurAmount) { + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(occurAmount < 0); + } + + private void validPrePaymentInput(PrePaymentInput input) { + //礼物打赏可以自己打赏自己的AI,需要排除 + if (!BizType.GIFT.equals(input.getBizType())) { + ToastResultCode.PAY_TRADE_PAYER_PAYEE_ERROR.check(Objects.equals(input.getSrcAccountId().toString(), input.getDesAccountNo())); + } + if (input.getOccurAmount() != null) { + validOccurAmount(input.getOccurAmount()); + } + if (input.getResourceNum() != null && input.getResourceNum() > 0) { + ToastResultCode.RESOURCEKEY_IS_BLANK.check(StringUtils.isBlank(input.getResourceKey())); + ToastResultCode.RESOURCENUM_EXCEEDS_THE_MAXIMUM.check(payTradeDao.countPaidByResourceKey(input.getResourceKey()) >= input.getResourceNum()); + } + } + + private PayTradeService getProxy() { + return (PayTradeService) AopContext.currentProxy(); + } + + @Override + public int getToWithdrawableIncomeTime(Long userId, CoinType coinType) { + //设置提现等待时间 + return 30; + } + + /** + * 平台 赠送金额 + * + * @param + * @return + */ + + @Transactional + @Override + public PayTrade platformGift(PlatformGiftInput req) { + BizType bizType = req.getType().getBizType(); + Long gift = req.getAmount(); + BuffType targetBuffType = BuffType.valueOf(req.getType().getBuffType().name()); + String tradeNo = KeyGenerator.instance().generatorUniqueKey(PayGenrtatorCodeType.getTradeType(siteType)); + PrePaymentInput input = PrePaymentInput.builder() + .name(req.getName()) + .outTradeNo(req.getOutTradeNo()) + .ip(req.getIp()) + .platform("Epal") + .payChannel(PayChannel.BUFF) + .bizType(bizType) + .desAccountNo(req.getUid().toString()) + .desAccountName(req.getName()) + .paymentType(PaymentType.BALANCE) + .srcAccountId(SystemUser.SYSTEM_USER_1.getValue()) + .srcAccountName("SYSTEM") + .productAmount(gift) + .extend(req.getExtend() == null ? "" : req.getExtend()) + .occurAmount(gift).build(); + + PayTrade payTrade = new PayTrade(); + payTrade.setIp(input.getIp()); + payTrade.setPlatform(input.getPlatform()); + payTrade.setOutTradeNo(tradeNo); + payTrade.setOutTradeNoRelationNo(input.getOutTradeNoRelationNo()); + payTrade.setTradeNo(tradeNo); + payTrade.setSrcAccountId(input.getSrcAccountId()); + payTrade.setSrcAccountName(input.getSrcAccountName()); + payTrade.setDesAccountNo(input.getDesAccountNo()); + payTrade.setDesAccountName(input.getDesAccountName()); + payTrade.setBizType(input.getBizType()); + payTrade.setName(input.getName()); + payTrade.setAmount(input.getProductAmount()); + payTrade.setBizType(bizType); + payTrade.setFee(input.getFee()); + payTrade.setPromoAmount(input.getPromoAmount()); + payTrade.setOccurAmount(input.getOccurAmount()); + payTrade.setPaymentTradeNo(input.getPaymentTradeNo()); + payTrade.setStatus(TradeStatus.FINISHED); + payTrade.setStatusInTime(LocalDateTime.now()); + + payTrade.setResourceKey(input.getResourceKey()); + payTrade.setResourceNum(input.getResourceNum()); + payTrade.setExtend(input.getExtend()); + if (input.getPayChannel() != null) { + payTrade.setPayChannel(input.getPayChannel()); + } + if (input.getPaymentType() != null) { + payTrade.setPaymentType(input.getPaymentType()); + } + + + if (req.getType().isNeedConfirm()) { //5.9.0 版本 送的BUFF需要用户确认 + payTrade.setStatus(TradeStatus.WAITPAY); + } else { + payTrade.setCloseTime(input.getCloseTime()); + payTrade.setPayTime(LocalDateTime.now()); + payTrade.setFinishTime(LocalDateTime.now()); + } + payTradeDao.insert(payTrade); + + BuffTransferInput buffTransferInput = BuffTransferInput.builder() + .platform("Epal") + .decUid(SystemUser.SYSTEM_USER_1.getValue()) + .decBuffType(BuffType.BALANCE) + .decBuff(gift) + .tradeNo(payTrade.getTradeNo()) + .bizType(bizType) + .bizNoRelationNo(payTrade.getTradeNo()) + .bizNo(payTrade.getTradeNo()) + .errorMessage(req.getReason()) + .payChannel(payTrade.getPayChannel()) + .build(); + + buffTransferInput.setAddUid(req.getUid()); + buffTransferInput.setAddBuffType(targetBuffType); + buffTransferInput.setAddBuff(gift); + + if (!req.getType().isNeedConfirm()) { + accountBuffService.transfer(buffTransferInput); + } + return payTrade; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PreChargeHandlerServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PreChargeHandlerServiceImpl.java new file mode 100644 index 0000000..b471fc5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/PreChargeHandlerServiceImpl.java @@ -0,0 +1,168 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.domain.entity.PayCharge; +import com.sonic.lion.domain.enums.SystemUser; +import com.sonic.lion.domain.input.PrePaymentInput; +import com.sonic.lion.domain.output.PrePaymentOutput; +import com.sonic.lion.domain.req.PreChargeReq; +import com.sonic.lion.domain.resp.PrePaymentResp; +import com.sonic.lion.enums.*; +import com.sonic.lion.service.*; +import com.sonic.lion.utils.LimitUtils; +import com.sonic.lion.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + + +@Slf4j +@Service +public class PreChargeHandlerServiceImpl implements PreChargeHandlerService { + + /** + * 30分钟后关闭交易 + */ + private static final int CHARGE_PAYMENT_MINUTES = 30; + + + @Value("${apple.paymentPlatformFeeRate}") + private BigDecimal applePaymentPlatformFeeRate; + @Value("${google.paymentPlatformFeeRate}") + private BigDecimal googlePaymentPlatformFeeRate; + + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private LimitUtils limitUtils; + @Autowired + private PayTradeService payTradeService; + @Autowired + private AccountBuffService accountBuffService; + @Autowired + private GoogleService googleService; + @Autowired + private PayChargeService payChargeService; + + @Override + public PrePaymentResp preCharge(Long userId, PreChargeReq req, String ip) { + //限流处理 + Boolean bl = limitUtils.defaultLimitCheckByKey(redisKeyUtils.createPrePayTrade(userId), 100, 3600); + ToastResultCode.SYSTEM_EXCEPTION.check(bl, "operate too frequently,please try again later (102)"); + //根据商品ID获取价格 + PayCharge payCharge = payChargeService.getByBundleIdAndProductIdAndPlatform(req.getProductId(), "web"); + ToastResultCode.DATA_NOT_EXITS.check(payCharge == null); + long occurAmount = payCharge.getPayAmount(); + + PrePaymentInput input = PrePaymentInput.builder() + .platform(Platform.PAY.getDesc()) + .bizType(BizType.CHARGE) + .name(BizType.CHARGE.getDesc()) + .srcAccountId(userId) + .desAccountNo(String.valueOf(SystemUser.SYSTEM_USER_1.getValue())) + .productAmount(payCharge.getPayAmount()) + .actualAmount(payCharge.getChargeAmount()) + .occurAmount(occurAmount) + .srcAccountName("") + .ip(ip) + .productId(req.getProductId()) + .payChannel(PayChannel.STRIPE) + .clientVersion(null) + .closeTime(LocalDateTime.now().plusMinutes(CHARGE_PAYMENT_MINUTES)) + .build(); + + PrePaymentOutput prePaymentOutput = payTradeService.prePayment(input); + return PrePaymentResp.builder() + .tradeNo(prePaymentOutput.getTradeNo()) + .tradeStatus(prePaymentOutput.getTradeStatus()) + .build(); + } + + @Override + public PrePaymentResp iapPreCharge(Long userId, PreChargeReq req, String ip) { + //限流处理 + Boolean bl = limitUtils.defaultLimitCheckByKey(redisKeyUtils.createPrePayTrade(userId), 100, 3600); + ToastResultCode.SYSTEM_EXCEPTION.check(bl, "operate too frequently,please try again later (102)"); + + PayCharge payCharge = payChargeService.getByBundleIdAndProductIdAndPlatform(req.getProductId(), "iOS"); + ToastResultCode.DATA_NOT_EXITS.check(payCharge == null); + long occurAmount = payCharge.getPayAmount(); + //计算总的手续费 + long fee = googleService.calculatePaymentFee(payCharge); + //计算平台抽成 + long platformFee = new BigDecimal(occurAmount + 1).multiply(applePaymentPlatformFeeRate).longValue() - 1; + platformFee = platformFee > 0 ? platformFee : 0; + //计算三方手续费 + long thirdFee = fee - platformFee; + PrePaymentInput input = PrePaymentInput.builder() + .platform(Platform.PAY.getDesc()) + .bizType(BizType.CHARGE) + .name(BizType.CHARGE.getDesc()) + .srcAccountId(userId) + .desAccountNo(String.valueOf(SystemUser.SYSTEM_USER_1.getValue())) + .productAmount(payCharge.getPayAmount()) + .actualAmount(payCharge.getChargeAmount()) + .payChannel(PayChannel.IAP) + .occurAmount(occurAmount) + .fee(fee) + .platformFee(platformFee) + .thirdFee(thirdFee) + .srcAccountName("") + .paymentType(PaymentType.CHANNEL) + .productId(req.getProductId()) + .clientVersion(null) + .ip(ip) + .build(); + PrePaymentOutput prePaymentOutput = payTradeService.prePayment(input); + return PrePaymentResp.builder() + .tradeNo(prePaymentOutput.getTradeNo()) + .tradeStatus(prePaymentOutput.getTradeStatus()) + .build(); + } + + @Override + public PrePaymentResp googlePreCharge(Long userId, PreChargeReq req, String ip) { + //限流处理 + Boolean bl = limitUtils.defaultLimitCheckByKey(redisKeyUtils.createPrePayTrade(userId), 100, 3600); + ToastResultCode.SYSTEM_EXCEPTION.check(bl, "operate too frequently,please try again later (102)"); + //根据产品ID获取价格相关的数据 + PayCharge payCharge = payChargeService.getByBundleIdAndProductIdAndPlatform(req.getProductId(), "android"); + ToastResultCode.DATA_NOT_EXITS.check(payCharge == null); + long occurAmount = payCharge.getPayAmount(); + //计算总的手续费 + long fee = googleService.calculatePaymentFee(payCharge); + //计算平台抽成 + long platformFee = new BigDecimal(occurAmount + 1).multiply(googlePaymentPlatformFeeRate).longValue() - 1; + platformFee = platformFee > 0 ? platformFee : 0; + //计算三方手续费 + long thirdFee = fee - platformFee; + PrePaymentInput input = PrePaymentInput.builder() + .platform(Platform.PAY.getDesc()) + .bizType(BizType.CHARGE) + .name(BizType.CHARGE.getDesc()) + .srcAccountId(userId) + .desAccountNo(String.valueOf(SystemUser.SYSTEM_USER_1.getValue())) + .productAmount(payCharge.getPayAmount()) + .actualAmount(payCharge.getChargeAmount()) + .payChannel(PayChannel.GOOGLE) + .occurAmount(occurAmount) + .fee(fee) + .platformFee(platformFee) + .thirdFee(thirdFee) + .srcAccountName("") + .paymentType(PaymentType.CHANNEL) + .productId(req.getProductId()) + .clientVersion(null) + .ip(ip) + .build(); + PrePaymentOutput prePaymentOutput = payTradeService.prePayment(input); + return PrePaymentResp.builder() + .tradeNo(prePaymentOutput.getTradeNo()) + .tradeStatus(prePaymentOutput.getTradeStatus()) + .build(); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingChargeServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingChargeServiceImpl.java new file mode 100644 index 0000000..86d73f9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingChargeServiceImpl.java @@ -0,0 +1,62 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.dao.PayCallChannelRecordDao; +import com.sonic.lion.dao.ProcessingChargeDao; +import com.sonic.lion.domain.entity.PayCallChannelRecord; +import com.sonic.lion.domain.entity.ProcessingCharge; +import com.sonic.lion.domain.output.SyncChannelOutput; +import com.sonic.lion.service.ChannelProcessingService; +import com.sonic.lion.service.ProcessingChargeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class ProcessingChargeServiceImpl implements ProcessingChargeService { + + @Autowired + private ProcessingChargeDao processingChargeDao; + + @Autowired + private ChannelProcessingService channelProcessingService; + + @Autowired + private PayCallChannelRecordDao payCallChannelRecordDao; + + @Override + public void hand() { + //查询除payssion外的其他渠道待处理充值数据进行处理 + List list = processingChargeDao.findOtherChannelAll(); + //查询payssion渠道的待处理充值数据进行处理,每次只取50条数据进行处理 + List payssionList = processingChargeDao.findPayssionChannelAll(); + //将数据列表中的数据进行合并处理 + list.addAll(payssionList); + if (CollectionUtils.isEmpty(list)) { + return; + } + log.info("handProcessingOrders size {}", list.size()); + List payCallChannelRecords = payCallChannelRecordDao.selectBatchIds(list.stream().map(ProcessingCharge::getCallChannelRecordId).collect(Collectors.toList())); + Map map = payCallChannelRecords.stream().collect(Collectors.toMap(PayCallChannelRecord::getId, record -> record)); + for (ProcessingCharge processingCharge : list) { + hand(map, processingCharge); + } + } + + private void hand(Map map, ProcessingCharge processingCharge) { + try { + PayCallChannelRecord record = map.get(processingCharge.getCallChannelRecordId()); + SyncChannelOutput syncChannelOutput = channelProcessingService.chargeProcessing(record); + log.info("handProcessingOrders result {}", syncChannelOutput); + } catch (Exception e) { + log.error("handProcessingOrders fail", e); + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingDisputeServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingDisputeServiceImpl.java new file mode 100644 index 0000000..6f205ac --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingDisputeServiceImpl.java @@ -0,0 +1,66 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.dao.ProcessingDisputeDao; +import com.sonic.lion.domain.entity.ProcessingDispute; +import com.sonic.lion.service.ProcessingDisputeService; +import com.sonic.lion.service.RefundService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Service +public class ProcessingDisputeServiceImpl implements ProcessingDisputeService { + + @Autowired + private ProcessingDisputeDao processingDisputeDao; + + + @Autowired + private RefundService refundService; + + public void hand() { + //查询待处理的50条数据进行处理 + List list = processingDisputeDao.selectList(Wrappers.lambdaQuery() + .eq(ProcessingDispute::getStatus, 0) + .orderByDesc(ProcessingDispute::getId) + .last("limit 50")); + if (CollectionUtils.isEmpty(list)) { + return; + } + log.info("handProcessingDispute size {}", list.size()); + for (ProcessingDispute processingDispute : list) { + hand(processingDispute); + } + } + + private void updateErrorStatus(Long id, String errorMessage) { + ProcessingDispute updateProcessingDispute = new ProcessingDispute(); + updateProcessingDispute.setId(id); + updateProcessingDispute.setStatus(2); + updateProcessingDispute.setError(errorMessage); + updateProcessingDispute.setEditTime(LocalDateTime.now()); + processingDisputeDao.updateById(updateProcessingDispute); + } + + + public void hand(ProcessingDispute processingDispute) { + try { + log.info("handProcessingDispute data {}", processingDispute); + boolean success = refundService.processingDisupte(processingDispute); + if (success) { + processingDisputeDao.deleteById(processingDispute.getId()); + } else { + updateErrorStatus(processingDispute.getId(), String.valueOf(success)); + } + } catch (Exception e) { + updateErrorStatus(processingDispute.getId(), e.getMessage()); + log.error("handProcessingOrders fail", e); + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingWithdrawReviewServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingWithdrawReviewServiceImpl.java new file mode 100644 index 0000000..68c1398 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingWithdrawReviewServiceImpl.java @@ -0,0 +1,62 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.dao.PayCallChannelRecordDao; +import com.sonic.lion.dao.PayTradeDao; +import com.sonic.lion.dao.ProcessingWithdrawReviewDao; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.entity.ProcessingWithdrawReview; +import com.sonic.lion.service.ProcessingWithdrawReviewService; +import com.sonic.lion.service.WithdrawService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class ProcessingWithdrawReviewServiceImpl implements ProcessingWithdrawReviewService { + + @Autowired + private ProcessingWithdrawReviewDao processingWithdrawReviewDao; + + @Autowired + private WithdrawService withdrawService; + + @Autowired + private PayTradeDao payTradeDao; + + + @Autowired + private PayCallChannelRecordDao payCallChannelRecordDao; + + @Override + public void hand() { + List list = processingWithdrawReviewDao.findAll(LocalDateTime.now().plusDays(-1) ); + if (CollectionUtils.isEmpty(list)) { + return; + } + log.info("handProcessingWithdrawReview size {}", list.size()); + List payTrades = payTradeDao.selectBatchIds(list.stream().map(ProcessingWithdrawReview::getTradeId).collect(Collectors.toList())); + Map map = payTrades.stream().collect(Collectors.toMap(PayTrade::getId, record -> record)); + + for (ProcessingWithdrawReview processingWithdraw : list) { + hand(map, processingWithdraw); + } + } + + private void hand(Map map, ProcessingWithdrawReview processingWithdraw) { + try { + PayTrade payTrade = map.get(processingWithdraw.getTradeId()); + log.error("handProcessingWithdrawReview ...{}", processingWithdraw); + withdrawService.handWithdrawWhenReviewTimeOut(payTrade); + processingWithdrawReviewDao.deleteById(processingWithdraw.getId()); + } catch (Exception e) { + log.error("handProcessingWithdrawReview fail", e); + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingWithdrawServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingWithdrawServiceImpl.java new file mode 100644 index 0000000..10eb1d2 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ProcessingWithdrawServiceImpl.java @@ -0,0 +1,106 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.dao.PayCallChannelRecordDao; +import com.sonic.lion.dao.ProcessingWithdrawDao; +import com.sonic.lion.domain.entity.PayCallChannelRecord; +import com.sonic.lion.domain.entity.ProcessingWithdraw; +import com.sonic.lion.domain.enums.CallChannelStatus; +import com.sonic.lion.domain.output.SyncChannelOutput; +import com.sonic.lion.service.ProcessingWithdrawService; +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class ProcessingWithdrawServiceImpl implements ProcessingWithdrawService { + + @Autowired + private ProcessingWithdrawDao processingWithdrawDao; + + @Autowired + private TradeHandler tradeHandler; + + + @Autowired + private PayCallChannelRecordDao payCallChannelRecordDao; + + /** + * 最大增加的下次处理时间增加的分钟数 + */ + private static Integer MAX_NEXT_TIME = 60; + + /** + * 处理次数和下次处理时间增加分钟数的映射关系 + */ + private static Map HANDLER_COUNT_TIME_MAP = Maps.newHashMap(); + + static { + //key 处理次数,value 下次处理时间增加的分钟数 + HANDLER_COUNT_TIME_MAP.put(0, 1); + HANDLER_COUNT_TIME_MAP.put(1, 1); + HANDLER_COUNT_TIME_MAP.put(2, 3); + HANDLER_COUNT_TIME_MAP.put(3, 5); + HANDLER_COUNT_TIME_MAP.put(4, 10); + HANDLER_COUNT_TIME_MAP.put(5, 15); + HANDLER_COUNT_TIME_MAP.put(6, 20); + HANDLER_COUNT_TIME_MAP.put(7, 25); + HANDLER_COUNT_TIME_MAP.put(8, 30); + HANDLER_COUNT_TIME_MAP.put(9, 60); + } + + @Override + public void hand() { + //查询出下次处理时间小于当前时间的数据进行处理,每次最多查询50条数据进行处理 + List list = processingWithdrawDao.findAll(); + if (CollectionUtils.isEmpty(list)) { + return; + } + log.info("handProcessingWithdraw size {}", list.size()); + List payCallChannelRecords = payCallChannelRecordDao.selectBatchIds(list.stream().map(ProcessingWithdraw::getCallChannelRecordId).collect(Collectors.toList())); + Map map = payCallChannelRecords.stream().collect(Collectors.toMap(PayCallChannelRecord::getId, record -> record)); + + for (ProcessingWithdraw processingWithdraw : list) { + hand(map, processingWithdraw); + } + } + + private void hand(Map map, ProcessingWithdraw processingWithdraw) { + try { + PayCallChannelRecord payCallChannelRecord = map.get(processingWithdraw.getCallChannelRecordId()); + log.info("handProcessingWithdraw ing ....{}", payCallChannelRecord); + + //根据次数来增加时间 + Integer handCount = processingWithdraw.getHandCount() == null ? 0 : processingWithdraw.getHandCount() + 1; + Integer addTimeSeconds = HANDLER_COUNT_TIME_MAP.get(handCount); + if(addTimeSeconds == null) { + addTimeSeconds = MAX_NEXT_TIME; + } + LocalDateTime nextHandTime = processingWithdraw.getNextHandTime() == null ? + LocalDateTime.now() : processingWithdraw.getNextHandTime().plusMinutes(addTimeSeconds); + ProcessingWithdraw updateProcessingWithdraw = new ProcessingWithdraw(); + updateProcessingWithdraw.setId(processingWithdraw.getId()); + updateProcessingWithdraw.setNextHandTime(nextHandTime); + updateProcessingWithdraw.setHandCount(handCount); + processingWithdrawDao.updateById(updateProcessingWithdraw); + + //调用第三方查询确认转账交易的最终执行结果 + SyncChannelOutput syncChannelOutput = tradeHandler.syncOrder(payCallChannelRecord); + if(CallChannelStatus.PROCESSING.equals(syncChannelOutput.getCallChannelStatus()) || CallChannelStatus.INIT.equals(syncChannelOutput.getCallChannelStatus())){ + //状态为处理中或初始化则不做任何处理 + }else{ + //其余状态直接删除掉待处理数据 + processingWithdrawDao.deleteById(processingWithdraw.getId()); + } + } catch (Exception e) { + log.error("handProcessingWithdraw fail", e); + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ReceiptHandlerServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ReceiptHandlerServiceImpl.java new file mode 100644 index 0000000..c39009d --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/ReceiptHandlerServiceImpl.java @@ -0,0 +1,69 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.domain.entity.GoogleUploadReceipt; +import com.sonic.lion.domain.entity.IapUploadReceipt; +import com.sonic.lion.domain.input.UploadReceiptInput; +import com.sonic.lion.domain.req.UploadReceiptReq; +import com.sonic.lion.service.*; +import com.sonic.lion.utils.MD5Utils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; + +import java.time.LocalDateTime; + +@Slf4j +@Service +public class ReceiptHandlerServiceImpl implements ReceiptHandlerService { + + @Autowired + private IapUploadReceiptService iapUploadReceiptService; + @Autowired + private IapService iapService; + @Autowired + private GoogleService googleService; + @Autowired + private GoogleUploadReceiptService googleUploadReceiptService; + + @Override + public void uploadIosSubReceipt(Long userId, UploadReceiptInput input) { + String md5 = MD5Utils.stringToMD5(input.getReceipt()); + IapUploadReceipt receipt = IapUploadReceipt.builder().receipt(input.getReceipt()).transactionsJsonStr(md5).userId(userId).createTime(LocalDateTime.now()).build(); + Long receiptId = null; + try { + receiptId = iapUploadReceiptService.save(receipt); + iapService.handReceiptSubscribe(input.getProductId(), receiptId, userId); + } catch (org.springframework.dao.DuplicateKeyException e) { + log.info("IOS应用商店订购上传凭据 md5:{} userId:{} 重复", md5, userId); + } catch (Exception e2) { + if (e2 instanceof RestClientException) { + try { + Thread.sleep(300L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + iapService.handReceiptSubscribe(input.getProductId(), receiptId, userId); + } + log.error("IOS应用商店订购上传凭据错误userId:" + userId, e2); + } + } + + @Override + public void uploadGoogleSubReceipt(Long userId, UploadReceiptReq input) { + String md5 = MD5Utils.stringToMD5(input.getReceipt()); + try { + GoogleUploadReceipt googleUploadReceipt = GoogleUploadReceipt.builder() + .transactionsJsonStr(md5) + .receipt(input.getReceipt()) + .processed(Boolean.FALSE) + .build(); + Long receiptId = googleUploadReceiptService.save(googleUploadReceipt); + googleService.handReceiptSubscribe(input.getProductId(), receiptId, userId); + } catch (org.springframework.dao.DuplicateKeyException e) { + log.info("Google上传收据 md5:{} 重复", md5); + } catch (Exception e1) { + log.error("Google上传收据:错误", e1); + } + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/RefundServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/RefundServiceImpl.java new file mode 100644 index 0000000..98644a7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/RefundServiceImpl.java @@ -0,0 +1,227 @@ +package com.sonic.lion.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.api.services.androidpublisher.model.VoidedPurchase; +import com.sonic.common.rpc.Page; +import com.sonic.daosupport.converter.PageConverter; +import com.sonic.lion.dao.AppleRefundRecordDao; +import com.sonic.lion.dao.PayCallChannelRecordDao; +import com.sonic.lion.dao.PayTradeDao; +import com.sonic.lion.domain.entity.*; +import com.sonic.lion.domain.enums.CallChannelStatus; +import com.sonic.lion.domain.input.ChannelRefundInput; +import com.sonic.lion.domain.output.ChannelRefundOutput; +import com.sonic.lion.domain.req.RefundQueryReq; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.TradeStatus; +import com.sonic.lion.service.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; + +@Slf4j +@Service +public class RefundServiceImpl implements RefundService { + + @Autowired + private GoogleService googleService; + + @Autowired + private StripeServiceImpl stripeService; + + @Autowired + private AppleRefundRecordDao appleRefundRecordDao; + + @Autowired + private GoogleRecordService googleRecordService; + + @Autowired + private PayTradeService payTradeService; + + @Autowired + private PayCallChannelRecordDao payCallChannelRecordDao; + + @Autowired + private AccountBuffService accountBuffService; + + @Autowired + private PayTradeDao payTradeDao; + + @Autowired + private PayChannelRouterService payChannelRouterService; + + @Autowired + private ChannelBlacklistService channelBlacklistService; + + @Override + public void pull(LocalDateTime startTime) { + //从Google 拉取争议数据 并存储在数据库 + pullFromGoogle(startTime); + } + + public void pullFromGoogle(LocalDateTime startTime) { + try { + List googleRefund = googleService.voidedPurchases(startTime); + log.info("开始拉取google 退款数据,{},{}", startTime, googleRefund); + for (VoidedPurchase voidedPurchase : googleRefund) { + handleGoogleVoidedPurchaseDetail(voidedPurchase); + } + } catch (Exception e) { + log.error("拉取google 退款数据失败", e); + e.printStackTrace(); + } + } + + private void handleGoogleVoidedPurchaseDetail(VoidedPurchase voidedPurchase) { + try { + String orderId = (String) voidedPurchase.get("orderId"); + + Long cancellation_date_ms = Long.valueOf(voidedPurchase.get("voidedTimeMillis").toString()); + LocalDateTime cancellation_date = LocalDateTime.ofEpochSecond(cancellation_date_ms / 1000, 0, ZoneOffset.ofHours(8)); + + Long purchase_date_ms = Long.valueOf(voidedPurchase.get("purchaseTimeMillis").toString()); + LocalDateTime purchase_date = LocalDateTime.ofEpochSecond(purchase_date_ms / 1000, 0, ZoneOffset.ofHours(8)); + + AppleRefundRecord appleRefundRecord = AppleRefundRecord.builder() + .platform(AppleRefundRecord.Platform.GOOGLE) + .transactionId(orderId) + .cancellationDate(cancellation_date) + .purchaseDate(purchase_date).build(); + int count = appleRefundRecordDao.selectCount(Wrappers.lambdaQuery() + .eq(AppleRefundRecord::getTransactionId, orderId)); + //已经存在的数据 不再处理 + if (count > 0) { + return; + } + GoogleRecord googleRecord = googleRecordService.getByTransactionId(orderId); + if (googleRecord != null) { + appleRefundRecord.setProductId(googleRecord.getProductId()); + appleRefundRecord.setTradeNo(googleRecord.getTradeNo()); + PayTrade payTrade = payTradeService.getByTradeNo(googleRecord.getTradeNo()); + appleRefundRecord.setAmount(payTrade.getOccurAmount()); + appleRefundRecord.setUserId(payTrade.getSrcAccountId()); + appleRefundRecord.setBuff(payTrade.getAmount()); + appleRefundRecord.setContent(JSON.toJSONString(voidedPurchase)); + + //开始处理 冻结 + AppleRefundRecord.FronzenStatus fronzenStatus = accountBuffService.fronzenBalanceAndSendMessage(payTrade.getSrcAccountId(), payTrade.getAmount()); + appleRefundRecord.setFronzenStatus(fronzenStatus); + AccountBuff accountBuff = accountBuffService.getByUid(payTrade.getSrcAccountId()); + appleRefundRecord.setAfterFronzenBuff(accountBuff.getBalance()); + appleRefundRecord.setInsertTime(LocalDateTime.now()); + appleRefundRecordDao.insert(appleRefundRecord); + //将用户加入渠道黑名单中 + channelBlacklistService.addBlacklist(appleRefundRecord.getUserId(), PayChannel.STRIPE.name()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public Page list(RefundQueryReq refundQueryReq) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(refundQueryReq.getUserId() != null && refundQueryReq.getUserId() != 0, AppleRefundRecord::getUserId, refundQueryReq.getUserId()) + .eq(StringUtils.isNotBlank(refundQueryReq.getTradeNo()), AppleRefundRecord::getTradeNo, refundQueryReq.getTradeNo()) + .eq(StringUtils.isNotBlank(refundQueryReq.getTransactionId()), AppleRefundRecord::getTransactionId, refundQueryReq.getTransactionId()) + .gt(refundQueryReq.getStartTime() != null, AppleRefundRecord::getCancellationDate, refundQueryReq.getStartTime()) + .lt(refundQueryReq.getEndTime() != null, AppleRefundRecord::getCancellationDate, refundQueryReq.getEndTime()) + .orderByDesc(AppleRefundRecord::getCancellationDate); + + IPage appleRefundRecordList = appleRefundRecordDao.selectPage(PageConverter.buildQueryPage(refundQueryReq.getPage()), queryWrapper); + Page pageResult = PageConverter.convert(appleRefundRecordList); + return pageResult; + } + + + @Deprecated + @Override + public void paypalRefundByRecordId(Long recordId) { + PayCallChannelRecord payCallChannelRecord = payCallChannelRecordDao.selectById(recordId); + if (!payCallChannelRecord.getDisputed()) { + return; + } + PayTrade payTrade = payTradeService.getByTradeNo(payCallChannelRecord.getTradeNo()); + if (payCallChannelRecord.getChannel().equals(PayChannel.STRIPE) && !payTrade.getStatus().equals(TradeStatus.REFUNDED)) { + + ChannelRefundInput channelRefundInput = new ChannelRefundInput(); + channelRefundInput.setAmount(payTrade.getOccurAmount()); + channelRefundInput.setTransactionId(payCallChannelRecord.getTransactionId()); + ChannelRefundOutput channelRefundOutput = stripeService.refund(channelRefundInput); + if (channelRefundOutput.getStatus().equals(CallChannelStatus.SUCC)) { + payCallChannelRecordDao.updateStatus(recordId, CallChannelStatus.CANCEL.getValue()); + payTradeService.updateStatus(payTrade.getId(), TradeStatus.REFUNDED, TradeStatus.PROCESSING); + + PayCallChannelRecord r = new PayCallChannelRecord(); + r.setChannel(PayChannel.STRIPE); + r.setTradeNo(payTrade.getTradeNo()); + r.setBizType(BizType.REFUND); + r.setBatchId(channelRefundOutput.getBatchId()); + r.setTransactionId(channelRefundOutput.getTransactionId()); + r.setResult(channelRefundOutput.getResult()); + r.setStatus(channelRefundOutput.getStatus()); + r.setAmount(channelRefundInput.getAmount()); + r.setPayerId(""); + r.setNextCheckTime(LocalDateTime.now()); + r.setLastCheckTime(LocalDateTime.now()); + r.setCreateTime(LocalDateTime.now()); + r.setEditTime(LocalDateTime.now()); + r.setCheckNum(0); + payCallChannelRecordDao.insert(r); + + } + log.info("发生争议而且 未入账 已成功退款,{}", payCallChannelRecord); + } + } + + + @Override + public void tryRefund(PayTrade payTrade, PayCallChannelRecord payCallChannelRecord) { + if (!payTrade.getStatus().equals(TradeStatus.REFUNDED) && !payTrade.getStatus().equals(TradeStatus.FINISHED) && !payTrade.getStatus().equals(TradeStatus.PAID)) { + //如果不是 已经成功付款 已经成功退款 都开始退款流程。 + log.info("发现争议开始退款 :{}", payTrade.getTradeNo()); + PayChannelService payChannelService = payChannelRouterService.getPayChannelService(payTrade.getPayChannel()); + ChannelRefundInput channelRefundInput = new ChannelRefundInput(); + channelRefundInput.setAmount(payTrade.getOccurAmount()); + channelRefundInput.setTransactionId(payCallChannelRecord.getTransactionId()); + ChannelRefundOutput channelRefundOutput = payChannelService.refund(channelRefundInput); + if (channelRefundOutput.getStatus().equals(CallChannelStatus.SUCC)) { + payCallChannelRecordDao.updateStatus(payCallChannelRecord.getId(), CallChannelStatus.CANCEL.getValue()); + PayCallChannelRecord r = new PayCallChannelRecord(); + r.setChannel(payTrade.getPayChannel()); + r.setTradeNo(payTrade.getTradeNo()); + r.setBizType(BizType.REFUND); + r.setBatchId(channelRefundOutput.getBatchId()); + r.setTransactionId(channelRefundOutput.getTransactionId()); + r.setResult(channelRefundOutput.getResult()); + r.setStatus(channelRefundOutput.getStatus()); + r.setAmount(channelRefundInput.getAmount()); + r.setPayerId(""); + r.setNextCheckTime(LocalDateTime.now()); + r.setLastCheckTime(LocalDateTime.now()); + r.setCreateTime(LocalDateTime.now()); + r.setEditTime(LocalDateTime.now()); + r.setCheckNum(0); + payCallChannelRecordDao.insert(r); + payTradeDao.updateById(PayTrade.builder().id(payTrade.getId()).status(TradeStatus.REFUNDED).build()); + } + } else { + log.info("尝试退款但状态不对 :{}", payTrade.getTradeNo()); + } + } + + @Override + public boolean processingDisupte(ProcessingDispute processingDispute) { + return false; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/StripeServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/StripeServiceImpl.java new file mode 100644 index 0000000..7ece9b5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/StripeServiceImpl.java @@ -0,0 +1,519 @@ +package com.sonic.lion.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.sonic.common.exception.BizException; +import com.sonic.common.exception.SysException; +import com.sonic.lion.channel.config.StripeConfig; +import com.sonic.lion.domain.bo.SubscriptionBo; +import com.sonic.lion.domain.bo.WebhookBo; +import com.sonic.lion.domain.entity.PayAccountFundThird; +import com.sonic.lion.domain.entity.PayTrade; +import com.sonic.lion.domain.enums.CallChannelStatus; +import com.sonic.lion.domain.enums.TradeEvent; +import com.sonic.lion.domain.input.*; +import com.sonic.lion.domain.output.*; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.ThirdAccountType; +import com.sonic.lion.enums.ToastResultCode; +import com.sonic.lion.service.PayAccountFundThirdService; +import com.sonic.lion.service.PayChannelService; +import com.sonic.lion.utils.MoneyUtils; +import com.stripe.Stripe; +import com.stripe.exception.SignatureVerificationException; +import com.stripe.exception.StripeException; +import com.stripe.model.*; +import com.stripe.model.checkout.Session; +import com.stripe.net.Webhook; +import com.stripe.param.CustomerCreateParams; +import com.stripe.param.RefundCreateParams; +import com.stripe.param.SubscriptionListParams; +import com.stripe.param.TransferCreateParams; +import com.stripe.param.checkout.SessionCreateParams; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.List; +import java.util.Map; + +/** + * @author: code + * @date: 2025/05/19 + * @Description: + * @version: 1.0.0 + */ +@Slf4j +@Service("stripeService") +public class StripeServiceImpl implements PayChannelService, InitializingBean { + + @Autowired + private PayAccountFundThirdService payAccountFundThirdService; + + @Autowired + private StripeConfig stripeConfig; + + + @Override + public ChannelPaymentOutput payment(@NonNull ChannelPaymentInput input) { + ChannelPaymentOutput output = new ChannelPaymentOutput(); + PayTrade payTrade = input.getPayTrade(); + String returnUrl = input.getReturnUrl(); + if (returnUrl.contains("?")) { + returnUrl += "&submitId="; + } else { + returnUrl += "?submitId="; + } + returnUrl += input.getSubmitId(); + //查询三方表中绑定的stripe的客户id + PayAccountFundThird payAccountFundThird = payAccountFundThirdService.getByAccountIdAndAppType(payTrade.getSrcAccountId(), ThirdAccountType.STRIPE_PAYMENT); + if(payAccountFundThird == null || StringUtils.isEmpty(payAccountFundThird.getOpenId())) { + //在stripe创建客户号,并保存到数据库中 + ChannelCreateCustomerInput createCustomerInput = ChannelCreateCustomerInput.builder() + .userId(payTrade.getSrcAccountId()) + .payChannel(PayChannel.STRIPE).build(); + ChannelCreateCustomerOutput channelCreateCustomerOutput = createCustomer(createCustomerInput); + payAccountFundThird = payAccountFundThirdService.bindStripe(payTrade.getSrcAccountId(), channelCreateCustomerOutput.getOpenId()); + } + log.info("===> payment payAccountFundThird : {}", payAccountFundThird); + String customerId = payAccountFundThird.getOpenId(); + try { + SessionCreateParams params = SessionCreateParams.builder() + .addLineItem( + SessionCreateParams.LineItem.builder() + .setPriceData( + SessionCreateParams.LineItem.PriceData.builder() + .setCurrency("usd") + .setUnitAmount(payTrade.getAmount()) // 动态传入价格 + .setProductData( + SessionCreateParams.LineItem.PriceData.ProductData.builder() + .setName("购买服务") // 动态商品名称 + .putMetadata("submitId", input.getPayTrade().getTradeNo()) // 可选:元数据 + .build() + ) + .build() + ) + .setQuantity(1L) + .build()) + // 手续费 Line Item + .addLineItem( + SessionCreateParams.LineItem.builder() + .setPriceData( + SessionCreateParams.LineItem.PriceData.builder() + .setCurrency("usd") + .setUnitAmount(payTrade.getOccurAmount() - payTrade.getAmount()) // 手续费金额 + .setProductData( + SessionCreateParams.LineItem.PriceData.ProductData.builder() + .setName("手续费") // 显示为手续费 +// .setDescription("3% 平台服务费") // 可选:详细描述 + .putMetadata("type", "platform_fee") + .putMetadata("submitId", input.getPayTrade().getTradeNo()) + .build() + ) + .build() + ) + .setQuantity(1L) + .build() + ) + .setMode(SessionCreateParams.Mode.PAYMENT) + .setCustomer(customerId) + .setSuccessUrl(returnUrl) + .setCancelUrl(input.getCancelUrl()) + .setPaymentIntentData( + SessionCreateParams.PaymentIntentData.builder() + .setCaptureMethod(SessionCreateParams.PaymentIntentData.CaptureMethod.AUTOMATIC) + //保存支付方式 + .setSetupFutureUsage(SessionCreateParams.PaymentIntentData.SetupFutureUsage.OFF_SESSION) + .putMetadata("submitId", input.getPayTrade().getTradeNo()) + .build() + ) + //设置30分钟后过期(不设置,默认为24小时) + .setExpiresAt(Instant.now().plusSeconds(30 * 60).getEpochSecond()) + .build(); + Session session = Session.create(params); + log.info("Stripe payment result: {}", session); + output.setResult(session == null ? null : JSONObject.toJSONString(session)); + if (session != null) { + output.setBatchId(session.getPaymentIntent()); + output.setTransactionId(session.getPaymentIntent()); + output.setStatus(CallChannelStatus.PROCESSING); + output.setPaymentUrl(session.getUrl()); + } else { + output.setStatus(CallChannelStatus.FAIL); + } + } catch (Exception e) { + log.error("创建Stripe交易失败", e); + throw new BizException("", e.getMessage(), e); + } + return output; + } + + @Override + public ChannelPayoutOutput payout(ChannelPayoutInput input) { + ChannelPayoutOutput output = new ChannelPayoutOutput(); + + TransferCreateParams transferParam = TransferCreateParams.builder() + .setAmount(input.getAmount()) + .setCurrency("USD") + .setDestination(input.getDesAccountNo()) + .build(); + try { + Transfer result = Transfer.create(transferParam); + String str = JSON.toJSONString(result); + log.info("Stripe payout result: {}", str); + output.setStatus(CallChannelStatus.PROCESSING); + output.setBatchId(result.getId()); + output.setTransactionId(result.getId()); + output.setResult(str); + } catch (StripeException e) { + log.error("stripe transfer error", e); + output.setStatus(CallChannelStatus.FAIL); + output.setResult(e.getMessage()); + } + return output; + } + + @Override + public ChannelCheckOutput checkPayout(String channelSn) { + ChannelCheckOutput output = new ChannelCheckOutput(); + try { + Transfer result = Transfer.retrieve(channelSn); + //TODO 处理查询的交易状态 + String str = JSON.toJSONString(result); + log.info("Stripe check payout result: {}", str); + output.setStatus(CallChannelStatus.SUCC); + output.setTransactionId(result.getId()); + output.setResult(str); + } catch (StripeException e) { + log.error("Stripe check payout error", e); + output.setStatus(CallChannelStatus.UNKNOW); + output.setResult(e.getMessage()); + } + return output; + } + + @Override + public ChannelCheckOutput checkPayment(@NonNull ChannelCheckPaymentInput input) { + ChannelCheckOutput output = new ChannelCheckOutput(); + PaymentIntent result = null; + try { + result = paymentIntentRetrieve(input.getBatchId()); + if ("succeeded".equals(result.getStatus())) { + output.setBatchId(result.getId()); + output.setTransactionId(result.getId()); + output.setChannelEvent(TradeEvent.PAYMENT); + output.setStatus(CallChannelStatus.SUCC); + } + } catch (Exception e) { + log.error("Stripe check payment error", e); + } + String str = JSON.toJSONString(result); + log.info("Stripe check payment result: {}", str); + output.setResult(str); + return output; + } + + @Override + public ChannelCheckOutput checkRefund(ChannelCheckRefundInput input) { + return null; + } + + @Override + public ChannelPaymentOutput createSubPayment(String priceId, String customerId, String successUrl, String cancelUrl, Long trialPeriodDays) { + SessionCreateParams params = SessionCreateParams.builder() + .addLineItem( + SessionCreateParams.LineItem.builder() + .setPrice(priceId) + .setQuantity(1L) + .build()) + .setMode(SessionCreateParams.Mode.SUBSCRIPTION) + .setCustomer(customerId) + .setSuccessUrl(successUrl) + .setCancelUrl(cancelUrl) + //设置30分钟后过期(不设置,默认为24小时) + .setExpiresAt(Instant.now().plusSeconds(30 * 60).getEpochSecond()) + .build(); + if(trialPeriodDays > 0) { + params = SessionCreateParams.builder() + .addLineItem( + SessionCreateParams.LineItem.builder() + .setPrice(priceId) + .setQuantity(1L) + .build()) + //设置试用天数 + .setSubscriptionData(SessionCreateParams.SubscriptionData.builder().setTrialPeriodDays(trialPeriodDays).build()) + .setMode(SessionCreateParams.Mode.SUBSCRIPTION) + .setCustomer(customerId) + .setSuccessUrl(successUrl) + .setCancelUrl(cancelUrl) + //设置30分钟后过期(不设置,默认为24小时) + .setExpiresAt(Instant.now().plusSeconds(30 * 60).getEpochSecond()) + .build(); + } + Session session = null; + try { + session = Session.create(params); + } catch (StripeException e) { + log.error("===> createSubPayment error : ", e); + ToastResultCode.SYSTEM_EXCEPTION.check(true); + } + log.info("===> createSubPayment session : {}", session); + ChannelPaymentOutput output = new ChannelPaymentOutput(); + output.setPaymentUrl(session.getUrl()); + output.setTransactionId(session.getId()); + output.setBatchId(session.getId()); + return output; + } + + + @Override + public String retrieveSessionUrl(String sessionId) { + try { + if(StringUtils.isEmpty(sessionId)) { + return null; + } + // 检索 Session + Session retrievedSession = Session.retrieve("cs_test_a1jbDwOV6REEPkfJF2MndM9IYdZ9DSrcGfQwqz9ELYoOiEpOIWXoV9aBfU"); + // 检查状态 + String status = retrievedSession.getStatus(); + if ("open".equals(retrievedSession.getStatus())) { + return retrievedSession.getUrl(); + } + } catch (StripeException e) { + System.err.println("检索失败: " + e.getMessage()); + // 如果 Session 不存在或其他错误,URL 也无效 + } + return null; + } + + @Override + public SubscriptionBo checkSubPayment(ChannelSubPayoutInput input) { + try { + //查询最新的一条活跃数据 + SubscriptionListParams params = SubscriptionListParams.builder() + .setCustomer(input.getCustomerId()) + .setLimit(1L) + .setStatus(SubscriptionListParams.Status.ACTIVE) + .build(); + List list = Subscription.list(params).getData(); + //查询最新一条试用的数据 + SubscriptionListParams params1 = SubscriptionListParams.builder() + .setCustomer(input.getCustomerId()) + .setLimit(1L) + .setStatus(SubscriptionListParams.Status.TRIALING) + .build(); + List list1 = Subscription.list(params1).getData(); + list.addAll(list1); + if(CollectionUtils.isEmpty(list)) { + return null; + } + log.info("===> checkSubPayment list : {}", list); + List boList = Lists.newArrayList(); + for (Subscription subscription : list) { + SubscriptionBo bo = new SubscriptionBo(); + bo.setId(subscription.getId()); + bo.setCustomer(subscription.getCustomer()); + bo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); + bo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); + //获取是否自动续订 + bo.setCancelAtPeriodEnd(subscription.getCancelAtPeriodEnd()); + bo.setStatus(subscription.getStatus()); + //获取价格 + List itemList = subscription.getItems().getData(); + for (SubscriptionItem item : itemList) { + bo.setPriceId(item.getPrice().getId()); + boList.add(bo); + } + } + if(CollectionUtils.isNotEmpty(boList)) { + //排序处理,按照时间的降序排,取过期时间最长的一条数据 + boList.sort((s2, s1) -> s1.getCurrentPeriodEnd().compareTo(s2.getCurrentPeriodEnd())); + } + log.info("===> checkSubPayment boList : {}", boList); + return CollectionUtils.isEmpty(boList) ? null : boList.get(0); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + @Override + public Long calculatePaymentFee(@NonNull Long productAmount) { + return new BigDecimal(productAmount).multiply(stripeConfig.getPaymentFeeRate()).setScale(0, BigDecimal.ROUND_UP).add(new BigDecimal(stripeConfig.getPaymentFeeBase())).longValue(); + } + + @Override + public ChannelCreateCustomerOutput createCustomer(@NonNull ChannelCreateCustomerInput input) { + Map metadata = Maps.newHashMap(); + metadata.put("userId", input.getUserId().toString()); + CustomerCreateParams customerParams = CustomerCreateParams.builder() + .setName(input.getName()) + .setEmail(input.getEmail()) + .setMetadata(metadata) + .build(); + Customer customer = null; + try { + customer = Customer.create(customerParams); + } catch (StripeException e) { + log.error("==> Stripe createCustomer error", e); + ToastResultCode.SYSTEM_EXCEPTION.check(true); + } + ChannelCreateCustomerOutput output = new ChannelCreateCustomerOutput(); + output.setOpenId(customer.getId()); + output.setResult(JSON.toJSONString(customer)); + return output; + } + + @Override + public ChannelCancelOutput cancel(@NonNull String batchId) { + ChannelCancelOutput output = new ChannelCancelOutput(); + PaymentIntent result = null; + try { + PaymentIntent paymentIntent = paymentIntentRetrieve(batchId); + result = paymentIntent.cancel(); + output.setStatus(CallChannelStatus.CANCEL); + } catch (StripeException e) { + log.error("Stripe retrieve error", e); + } + String str = JSON.toJSONString(result); + log.info("Stripe cancel result: {}", str); + output.setResult(str); + return output; + } + + @Override + public ChannelRefundOutput refund(@NonNull ChannelRefundInput input) { + ChannelRefundOutput output = new ChannelRefundOutput(); + try { + Refund result = Refund.create(RefundCreateParams.builder() + .setPaymentIntent(input.getTransactionId()) + .build()); + String str = JSON.toJSONString(result); + output.setResult(str); + log.info("Stripe refund result: {}", str); + if ("succeeded".equals(result.getStatus())) { + output.setBatchId(result.getId()); + output.setTransactionId(result.getId()); + output.setStatus(CallChannelStatus.SUCC); + } else { + output.setStatus(CallChannelStatus.FAIL); + } + } catch (Exception e) { + log.error("Stripe refund error", e); + throw new BizException("", e.getMessage(), e); + } + return output; + } + + @Override + public ChannelWebhookOutput webhookAfterHandler(@NonNull ChannelWebhookInput input) { + String payload = (String) input.getPayload(); + String signature = (String) input.getSignature(); + ChannelWebhookOutput output = new ChannelWebhookOutput(); + try { + Event event = Webhook.constructEvent(payload, signature, stripeConfig.getWebhookSecret()); + EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); + StripeObject stripeObject = dataObjectDeserializer.getObject().get(); + String str = stripeObject.toJson(); + log.info("Stripe webhook result: {}", str); + output.setResult(str); + JSONObject jsonObject = JSONObject.parseObject(str); + output.setBatchId(jsonObject.getString("id")); + + if ("charge.succeeded".equals(event.getType()) || "payment_intent.succeeded".equals(event.getType())) { + output.setChannelEvent(TradeEvent.PAYMENT); + output.setStatus(CallChannelStatus.SUCC); + } else if ("transfer.paid".equals(event.getType())) { + output.setChannelEvent(TradeEvent.WITHDRAW); + output.setStatus(CallChannelStatus.SUCC); + } else if ("payment_method.attached".equals(event.getType())) { + PaymentMethod paymentMethod = (PaymentMethod) stripeObject; + PaymentMethod.Card card = paymentMethod.getCard(); + String openId = paymentMethod.getCustomer(); + Long accountId = payAccountFundThirdService.getAccountIdByOpenId(openId, ThirdAccountType.STRIPE_PAYMENT); + + } + } catch (Exception e) { + log.error("Stripe webhook签名校验异常", e); + throw new SysException("", e.getMessage(), e); + } + return output; + } + + @Override + public ChannelBindBankCardOutput bindBankCard(ChannelBindBankCardInput input) { + //TODO 创建 三方账号 + + return null; + } + + @Override + public ChannelBindPaypalOutput bindPayPal(ChannelBindPaypalInput input) { + return null; + } + + @Override + public ChannelBindBankAccountOutput bindBankAccount(ChannelBindBankAccountInput input) { + return null; + } + + @Override + public Long getPaymentFeeBase() { + return null; + } + + @Override + public BigDecimal getPaymentFeeRate() { + return null; + } + + @Override + public WebhookBo webhookBeforeHandler(@NonNull ChannelWebhookInput input, String webhookSecret) { + String payload = (String) input.getPayload(); + String signature = (String) input.getSignature(); + Event event; + try { + event = Webhook.constructEvent(payload, signature, webhookSecret); + log.info("===> webhookBeforeHandler event : {}", JSONObject.toJSONString(event)); + } catch (SignatureVerificationException e) { + log.error("Stripe webhook签名校验异常", e); + throw new SysException("", e.getMessage(), e); + } + JSONObject jsonObject = JSONObject.parseObject(payload); + WebhookBo bo = new WebhookBo(); + bo.setId(jsonObject.getJSONObject("data").getJSONObject("object").getString("id")); + String customer = jsonObject.getJSONObject("data").getJSONObject("object").getString("customer"); + bo.setCustomerId(customer); + bo.setEventType(event.getType()); + return bo; + } + + @Override + public Long calculateWithdrawFee(@NonNull Long occurAmount) { + return MoneyUtils.calculateFee(occurAmount, stripeConfig.getWithdrawFeeRate()) + 25L; + } + + private PaymentIntent paymentIntentRetrieve(String batchId) { + try { + PaymentIntent paymentIntent = PaymentIntent.retrieve(batchId); + log.info("Stripe retrieve result: {}", JSONObject.toJSONString(paymentIntent)); + return paymentIntent; + } catch (StripeException e) { + log.error("Stripe retrieve error", e); + throw new BizException("", e.getMessage(), e); + } + } + + @Override + public void afterPropertiesSet() { + Stripe.apiKey = stripeConfig.getApiKey(); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/StripeSubscribeServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/StripeSubscribeServiceImpl.java new file mode 100644 index 0000000..3fd4f2b --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/StripeSubscribeServiceImpl.java @@ -0,0 +1,181 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.dao.AppStoreProductDao; +import com.sonic.lion.dao.PayCallChannelRecordDao; +import com.sonic.lion.dao.UserSubscriptionDao; +import com.sonic.lion.domain.bo.SubscriptionBo; +import com.sonic.lion.domain.bo.WebhookBo; +import com.sonic.lion.domain.entity.PayAccountFundThird; +import com.sonic.lion.domain.entity.UserSubscription; +import com.sonic.lion.domain.input.ChannelCreateCustomerInput; +import com.sonic.lion.domain.input.ChannelSubPayoutInput; +import com.sonic.lion.domain.input.CreateSubscribeCheckSessionInput; +import com.sonic.lion.domain.output.ChannelCreateCustomerOutput; +import com.sonic.lion.domain.output.ChannelPaymentOutput; +import com.sonic.lion.domain.output.CreateSubscribeCheckoutSessionOutput; +import com.sonic.lion.domain.resp.AppProductInfoOutput; +import com.sonic.lion.enums.PayChannel; +import com.sonic.lion.enums.ThirdAccountType; +import com.sonic.lion.enums.ToastResultCode; +import com.sonic.lion.service.*; +import com.sonic.lion.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class StripeSubscribeServiceImpl implements StripeSubscribeService { + + @Autowired + private AppStoreProductDao appStoreProductDao; + @Autowired + private PayAccountFundThirdService payAccountFundThirdService; + @Autowired + @Qualifier("stripeService") + private PayChannelService stripeService; + @Autowired + private PayCallChannelRecordDao payCallChannelRecordDao; + @Autowired + private UserSubscriptionService userSubscriptionService; + @Autowired + private UserSubscriptionDao userSubscriptionDao; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private CommonMessageService commonMessageService; + + @Transactional(rollbackFor = Exception.class) + @Override + public CreateSubscribeCheckoutSessionOutput createSubscribeCheckoutSession(Long userId, CreateSubscribeCheckSessionInput input) { + //判断当前用户是否已经在订阅中了,如果已经订阅了则不能获取订阅链接 + int count = userSubscriptionDao.selectCount(Wrappers.lambdaQuery() + .eq(UserSubscription::getUserId, userId) + .eq(UserSubscription::getPlatform, PayChannel.STRIPE.name()) + .gt(UserSubscription::getExpTime, LocalDateTime.now())); + ToastResultCode.SUBSCRIBED_NO_DUPLICATE.check(count > 0); + + //查询三方表中绑定的stripe的客户id + PayAccountFundThird payAccountFundThird = payAccountFundThirdService.getByAccountIdAndAppType(userId, ThirdAccountType.STRIPE_PAYMENT); + if(payAccountFundThird == null || StringUtils.isEmpty(payAccountFundThird.getOpenId())) { + //在stripe创建客户号,并保存到数据库中 + ChannelCreateCustomerInput createCustomerInput = ChannelCreateCustomerInput.builder() + .userId(userId) + .payChannel(PayChannel.STRIPE).build(); + ChannelCreateCustomerOutput channelCreateCustomerOutput = stripeService.createCustomer(createCustomerInput); + payAccountFundThird = payAccountFundThirdService.bindStripe(userId, channelCreateCustomerOutput.getOpenId()); + } + String customerId = payAccountFundThird.getOpenId(); + //不生成多个支付连接,查询连接是否存在、链接是否失效。然后再返回给前端 + String sessionId = stringRedisTemplate.opsForValue().get(redisKeyUtils.subSessionIdCacheKey(userId)); + String url = stripeService.retrieveSessionUrl(sessionId); + if(StringUtils.isNotEmpty(sessionId) && StringUtils.isEmpty(url)) { + //删除缓存 + stringRedisTemplate.delete(redisKeyUtils.subSessionIdCacheKey(userId)); + } + if(StringUtils.isNotEmpty(url)) { + return CreateSubscribeCheckoutSessionOutput.builder().payUrl(url).build(); + } + //查询数据库获得 订阅价格 + AppProductInfoOutput appProduct = appStoreProductDao.queryAppProductInfo(input.getSubProductId()); + //商品不存在,直接抛出异常 + ToastResultCode.SUB_PRODUCT_NOT_FOUND.check(appProduct == null); + //设置免费试用天数(当用户首次订阅时才享受3天免费) + Long trialPeriodDays = userSubscriptionDao.getByUserId(userId) == null ? appProduct.getFreeDays() : 0L; + //创建订阅的结帐会话并生成支付url返回 + ChannelPaymentOutput paymentOutput = stripeService.createSubPayment(input.getSubProductId(), customerId, input.getReturnUrl(), input.getCancelUrl(), trialPeriodDays); + //存储到redis缓存中,30分钟后过期 + stringRedisTemplate.opsForValue().set(redisKeyUtils.subSessionIdCacheKey(userId), paymentOutput.getBatchId(), 30 * 60, TimeUnit.SECONDS); + return CreateSubscribeCheckoutSessionOutput.builder().payUrl(paymentOutput.getPaymentUrl()).build(); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void subscriptionHandler(Long notifyId, WebhookBo webhookBo) { + //查询关系数据,没有绑定关系的话直接抛出异常 + Long userId = payAccountFundThirdService.getAccountIdByOpenId(webhookBo.getCustomerId(), ThirdAccountType.STRIPE_PAYMENT); + if(userId == null) { + log.error("===> subscriptionHandler userId is null"); + return; + } + //处理用户订阅成功事件 + doWhenPay(notifyId, userId, webhookBo); + //处理用户取消订阅事件 + doWhenCancel(notifyId, userId, webhookBo); + } + + /** + * 处理用户订阅成功事件 + * @param notifyId + * @param userId + * @param webhookBo + */ + private void doWhenPay(Long notifyId, Long userId, WebhookBo webhookBo) { + //非创建、更新状态不进行处理 + if (!"customer.subscription.created".equals(webhookBo.getEventType()) && + !"customer.subscription.updated".equals(webhookBo.getEventType())) { + return; + } + try { + //从stripe中查询当前用户的订阅到期时间 + SubscriptionBo subscriptionBo = stripeService.checkSubPayment(ChannelSubPayoutInput.builder().customerId(webhookBo.getCustomerId()).build()); + log.info("===> webhook subscriptionBo : {}", subscriptionBo); + if(subscriptionBo == null || subscriptionBo.getCurrentPeriodEnd() == null + || System.currentTimeMillis() / 1000 > subscriptionBo.getCurrentPeriodEnd().intValue()) { + //没有过期时间,说明用户没有订阅,不做任何处理,直接关闭掉当前的回调 + userSubscriptionService.completeNotify(notifyId); + return; + } + //处理扣费失败的场景 + if("past_due".equalsIgnoreCase(subscriptionBo.getStatus())) { + //修改自动续订状态 为关闭 + userSubscriptionDao.updateAutoRenew(userId, false); + //完成数据处理 + userSubscriptionService.completeNotify(notifyId);; + //订阅扣费失败 + commonMessageService.memberRenewFail(userId); + return; + } + //判断状态不为订阅、活跃状态的话不进行后续处理 + if(!"active".equalsIgnoreCase(subscriptionBo.getStatus()) && !"trialing".equalsIgnoreCase(subscriptionBo.getStatus())) { + //完成数据处理 + userSubscriptionService.completeNotify(notifyId);; + return; + } + //更新用户订阅相关的基础信息 + userSubscriptionService.bindStripe(subscriptionBo); + //完成 + userSubscriptionService.completeNotify(notifyId); + log.info("===========> subscriptionHandler success"); + } catch (Exception e) { + log.error("===> webhook exception : ", e); + userSubscriptionService.failNotify(notifyId); + //抛出异常 + ToastResultCode.SYSTEM_EXCEPTION.check(true); + } + } + + private void doWhenCancel(Long notifyId, Long userId, WebhookBo webhookBo) { + //订阅被取消或到期时触发,自动续订停止 + if(!webhookBo.getEventType().equalsIgnoreCase("customer.subscription.deleted")) { + return; + } + //修改自动续订状态 为关闭 + userSubscriptionDao.updateAutoRenew(userId, false); + //更新到期时间为当前时间 + userSubscriptionDao.updateExpTimeByUserId(userId, LocalDateTime.now()); + //完成数据 + userSubscriptionService.completeNotify(notifyId); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/TradeHandler.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/TradeHandler.java new file mode 100644 index 0000000..843a994 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/TradeHandler.java @@ -0,0 +1,35 @@ +package com.sonic.lion.service.impl; + +import com.sonic.lion.dao.PayTradeDao; +import com.sonic.lion.domain.entity.PayCallChannelRecord; +import com.sonic.lion.domain.output.SyncChannelOutput; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.service.ChannelProcessingService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 交易处理 + */ +@Slf4j +@Component +public class TradeHandler { + + @Autowired + private PayTradeDao payTradeDao; + + @Autowired + private ChannelProcessingService channelProcessingService; + + public SyncChannelOutput syncOrder(PayCallChannelRecord record) { + if (BizType.CHARGE.equals(record.getBizType())) { + return channelProcessingService.chargeProcessing(record); + } + if (BizType.WITHDRAW.equals(record.getBizType())) { + return channelProcessingService.withdrawProcessing(record); + } + return null; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/UserSubscriptionServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/UserSubscriptionServiceImpl.java new file mode 100644 index 0000000..fc689c2 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/UserSubscriptionServiceImpl.java @@ -0,0 +1,302 @@ +package com.sonic.lion.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.google.common.collect.Lists; +import com.sonic.lion.dao.UserSubscriptionNotifyDao; +import com.sonic.lion.dao.UserSubscribeLogDao; +import com.sonic.lion.dao.UserSubscriptionDao; +import com.sonic.lion.domain.bo.SubscriptionBo; +import com.sonic.lion.domain.entity.AppStoreProduct; +import com.sonic.lion.domain.entity.UserSubscriptionNotify; +import com.sonic.lion.domain.entity.UserSubscribeLog; +import com.sonic.lion.domain.entity.UserSubscription; +import com.sonic.lion.enums.ThirdAccountType; +import com.sonic.lion.enums.ToastResultCode; +import com.sonic.lion.service.CommonMessageService; +import com.sonic.lion.service.PayAccountFundThirdService; +import com.sonic.lion.service.PayChargeService; +import com.sonic.lion.service.UserSubscriptionService; +import com.sonic.lion.utils.DateConvertUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.BooleanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author code + * 订阅相关 + */ +@Slf4j +@Service +public class UserSubscriptionServiceImpl implements UserSubscriptionService { + + @Autowired + private UserSubscriptionDao userSubscriptionDao; + @Autowired + private UserSubscriptionNotifyDao userSubscriptionNotifyDao; + @Autowired + private PayChargeService payChargeService; + @Autowired + private UserSubscribeLogDao userSubscribeLogDao; + @Autowired + private PayAccountFundThirdService payAccountFundThirdService; + @Autowired + private CommonMessageService commonMessageService; + + /** + * 增加订阅日志 + * + * @param appStoreProduct + * @param userSubscriptionDb + */ + private void addLogs(Long orgUserId, AppStoreProduct appStoreProduct, UserSubscription userSubscriptionDb) { + UserSubscribeLog userSubscribeLog = UserSubscribeLog.builder() + .orgUserId(orgUserId) + .userId(userSubscriptionDb.getUserId()).subscriptionId(userSubscriptionDb.getSubscriptionId()) + .platform(appStoreProduct.getPlatform()).buff(0L).payAmount(appStoreProduct.getPayAmount()) + .memberType(appStoreProduct.getMemberType()).period(appStoreProduct.getPeriod()).productId(appStoreProduct.getProductId()) + .createTime(LocalDateTime.now()).build(); + + if (userSubscriptionDb.getPlatform().equals(UserSubscription.Platform.APPLE)) { + userSubscribeLog.setAppStoreAmount(BigDecimal.valueOf(userSubscribeLog.getPayAmount()).multiply(new BigDecimal("0.3")).setScale(0, BigDecimal.ROUND_HALF_UP).longValue()); + } else if (userSubscriptionDb.getPlatform().equals(UserSubscription.Platform.GOOGLE)) { + userSubscribeLog.setAppStoreAmount(BigDecimal.valueOf(userSubscribeLog.getPayAmount()).multiply(new BigDecimal("0.15")).setScale(0, BigDecimal.ROUND_HALF_UP).longValue()); + } else if (userSubscriptionDb.getPlatform().equals(UserSubscription.Platform.STRIPE)) { + //pay_amount * 0.029 + 30 + userSubscribeLog.setAppStoreAmount(BigDecimal.valueOf(userSubscribeLog.getPayAmount()).multiply(new BigDecimal("0.029")).setScale(0, BigDecimal.ROUND_HALF_UP).longValue() + 30); + } + userSubscribeLogDao.insert(userSubscribeLog); + } + + @Override + public void bindStripe(SubscriptionBo subscriptionBo) { + ToastResultCode.DATA_NOT_EXITS.check(subscriptionBo == null); + ToastResultCode.SUBSCRIPTION_STATUS_ERROR.check(subscriptionBo.getCurrentPeriodEnd() <= System.currentTimeMillis() / 1000); + //根据客户查询用户ID + Long userId = payAccountFundThirdService.getAccountIdByOpenId(subscriptionBo.getCustomer(), ThirdAccountType.STRIPE_PAYMENT); + //查询商品详情 + AppStoreProduct appStoreProduct = payChargeService.getAppStoreProduct(subscriptionBo.getPriceId(), UserSubscription.Platform.STRIPE.getDesc()); + //获取不到商品的话直接抛出异常 + ToastResultCode.DATA_NOT_EXITS.check(appStoreProduct == null); + LocalDateTime purchaseDate = DateConvertUtils.getDate(subscriptionBo.getCurrentPeriodStart()); + //统一对结束时间做一个处理,+2个小时 + subscriptionBo.setCurrentPeriodEnd(subscriptionBo.getCurrentPeriodEnd() + (2 * 60 * 60)); + LocalDateTime expTime = DateConvertUtils.getDate(subscriptionBo.getCurrentPeriodEnd()); + this.bind(subscriptionBo.getId(), UserSubscription.Platform.STRIPE, expTime, purchaseDate, appStoreProduct.getProductId(), userId, null, null); + } + + /** + * 绑定 userId 关系 或 续期 + * + * @param subscriptionId + * @param platform + * @param exptime + * @param purchaseDate + * @param productId + * @param userId + * @param purchaseToken + * @param ip + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void bind(String subscriptionId, UserSubscription.Platform platform, LocalDateTime exptime, LocalDateTime purchaseDate, String productId, Long userId, String purchaseToken, String ip) { + if (exptime != null && LocalDateTime.now().isAfter(exptime)) { + log.info("===> sub bind 新过期时间不正确1, {}, {}", LocalDateTime.now(), exptime); + return; + } + //相差毫秒数 + long seconds = 0; + AppStoreProduct appStoreProduct = payChargeService.getAppStoreProduct(productId, platform.getDesc()); + UserSubscription userSubscriptionDb = userSubscriptionDao.getBySubscriptionId(subscriptionId); + //如果是Stripe渠道的话,根据用户ID来进行查询订阅所绑定的数据 + if(UserSubscription.Platform.STRIPE == platform) { + userSubscriptionDb = userSubscriptionDao.getByUserId(userId); + } + //看数据库有没有 已经存在的绑定 不存在就是新订阅的产品。 + if (userSubscriptionDb == null) { + UserSubscription userSubscription = UserSubscription.builder() + .userId(userId) + .productId(productId) + .purchaseToken(purchaseToken) + .memberType(appStoreProduct.getMemberType()) + .priceType(appStoreProduct.getPeriod()) + .purchaseTime(purchaseDate) + .autoRenewStatus(Boolean.TRUE) + .ip(ip) + .subscriptionId(subscriptionId).platform(platform).expTime(exptime).build(); + ToastResultCode.USER_ID_NOT_NULL.check(userId == null); + + userSubscriptionDao.insert(userSubscription); + userSubscriptionDb = userSubscription; + // 如果已经存在 说明之前 该用户订阅过本产品 。中间有过暂停 或者 是需要续期。 只需要更新续期的到期时间 并 向下游发送消息。 + Duration duration = Duration.between(LocalDateTime.now(), exptime); + //相差毫秒数 + seconds = duration.getSeconds(); + } else { + //幂等性处理 如果 当前数据库的时间 已经新报出的数据之后 。丢弃 + if (userSubscriptionDb.getExpTime().isAfter(exptime) || userSubscriptionDb.getExpTime().isEqual(exptime)) { + log.info("===> sub bind 新过期时间不正确2, {}, {}", userSubscriptionDb.getExpTime(), exptime); + return; + } + Duration duration = Duration.between(userSubscriptionDb.getExpTime().isAfter(LocalDateTime.now()) ? userSubscriptionDb.getExpTime() : LocalDateTime.now(), exptime); + //相差毫秒数 + seconds = duration.getSeconds(); + // 这里也可能是产品ID 变更了。升级了 + UserSubscription updateSub = UserSubscription.builder() + .id(userSubscriptionDb.getId()) + .subscriptionId(subscriptionId) + .userId(userId) + .purchaseToken(purchaseToken) + .purchaseTime(purchaseDate) + .editTime(LocalDateTime.now()) + .autoRenewStatus(Boolean.TRUE) + .productId(productId) + .expTime(exptime).build(); + userSubscriptionDao.updateById(updateSub); + } + Long orgUserId = userSubscriptionDb.getUserId(); + //重新查询出订阅的基础数据,以获取据库中全量的变化数据 + userSubscriptionDb = userSubscriptionDao.selectById(userSubscriptionDb.getId()); + log.info("===> sub bind orgUserId : {}, currentUserId : {}, seconds : {}", orgUserId, userSubscriptionDb.getUserId(), seconds); + //添加处理日志 + addLogs(orgUserId, appStoreProduct, userSubscriptionDb); + //发送mq消息到服务进行会员处理 + sendMessageWhenStatusChange(userSubscriptionDb, false); + } + + @Override + public UserSubscription getBySubscriptionId(String subscriptionId) { + return userSubscriptionDao.getBySubscriptionId(subscriptionId); + } + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Long addSubscriptionNotify(String subscriptionId, String messageId, String content, String + type, UserSubscriptionNotify.Platform platform) { + UserSubscriptionNotify userSubscriptionNotify = UserSubscriptionNotify.builder().subscriptionId(subscriptionId).messageId(messageId) + .content(content).type(type).platform(platform).status(UserSubscriptionNotify.STATUS.PROCESSING).build(); + userSubscriptionNotifyDao.insert(userSubscriptionNotify); + return userSubscriptionNotify.getId(); + } + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Long addSubscriptionNotifyV2(String subscriptionId, String messageId, String content, String extend, String + type, UserSubscriptionNotify.Platform platform) { + UserSubscriptionNotify userSubscriptionNotify = UserSubscriptionNotify.builder().subscriptionId(subscriptionId).messageId(messageId) + .content(content).extend(extend).type(type).platform(platform).status(UserSubscriptionNotify.STATUS.PROCESSING).build(); + userSubscriptionNotifyDao.insert(userSubscriptionNotify); + return userSubscriptionNotify.getId(); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void setPurchaseToken(Long id, String token) { + userSubscriptionDao.updateToken(id, token); + } + + /** + * 订阅状态变更 发送 MQ消息到下游 + * + * @param userSubscription + */ + public void sendMessageWhenStatusChange(UserSubscription userSubscription, boolean changeStatus) { + if(!changeStatus) { + //会员订阅成功同志消息处理 + commonMessageService.memberRenewSuccess(userSubscription.getUserId(), userSubscription.getExpTime()); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void cancel(String subscriptionId, UserSubscription.Platform platform, Long expTime) { + UserSubscription userSubscription = userSubscriptionDao.getBySubscriptionId(subscriptionId); + //更新数据库字段。 + userSubscriptionDao.updateById(UserSubscription.builder().id(userSubscription.getId()).autoRenewStatus(Boolean.FALSE).build()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void completeNotify(Long notifyId) { + userSubscriptionNotifyDao.updateStatus(notifyId, UserSubscriptionNotify.STATUS.FINISHED.name()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateStatusAndSubscriptionId(Long notifyId, String subscriptionId) { + userSubscriptionNotifyDao.updateStatusAndSubscriptionId(notifyId, UserSubscriptionNotify.STATUS.FINISHED.name(), subscriptionId); + } + + @Override + public void updateNotifyExtend(Long notifyId, Long appleRefundRecordId, String extend) { + userSubscriptionNotifyDao.updateNotifyExtend(notifyId, appleRefundRecordId, extend); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void failNotify(Long notifyId) { + userSubscriptionNotifyDao.updateStatus(notifyId, UserSubscriptionNotify.STATUS.FAIL.name()); + } + + + /** + * 续订失败发送系统消息 + * + * @param subscriptionId + * @param platform + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void payFailSystemMessage(String subscriptionId, UserSubscription.Platform platform) { + UserSubscription userSubscription = userSubscriptionDao.getBySubscriptionId(subscriptionId); + if (userSubscription == null || BooleanUtils.isTrue(userSubscription.getRemind())) { + return; + } + //发送系统消息 + commonMessageService.memberRenewFail(userSubscription.getUserId()); + //更新提醒字段 + userSubscriptionDao.updateRemind(userSubscription.getId()); + } + + @Transactional + @Override + public void updateStatusByThird(String subscriptionId, UserSubscription.Platform platform, String productId, Boolean autoRenew) { + log.info("变更订阅状态{} , {} ,{}", subscriptionId, productId, autoRenew); + UserSubscription userSubscription = userSubscriptionDao.getBySubscriptionId(subscriptionId); + AppStoreProduct appStoreProduct = payChargeService.getAppStoreProduct(productId, platform.getDesc()); + userSubscription.setProductId(productId); + userSubscription.setPriceType(appStoreProduct.getPeriod()); + userSubscription.setAutoRenewStatus(autoRenew); + userSubscription.setMemberType(appStoreProduct.getMemberType()); + if (userSubscription == null) { + return; + } + int rows = userSubscriptionDao.updateById(UserSubscription.builder().id(userSubscription.getId()) + .productId(productId) + .priceType(userSubscription.getPriceType()) + .memberType(userSubscription.getMemberType()) + .autoRenewStatus(autoRenew).build()); + // 状态变更了 + if (rows > 0) { + sendMessageWhenStatusChange(userSubscription, true); + } else { + log.info("变更订阅状态失败 数据库已经是这样了{} , {} ,{}", subscriptionId, productId, autoRenew); + } + } + + @Override + public List queryUserIsSubscribe(List userIdList) { + if (CollectionUtils.isEmpty(userIdList)) { + return Lists.newArrayList(); + } + return userSubscriptionDao.queryUserSubscription(userIdList); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/WebhookHandlerServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/WebhookHandlerServiceImpl.java new file mode 100644 index 0000000..40d14b4 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/WebhookHandlerServiceImpl.java @@ -0,0 +1,258 @@ +package com.sonic.lion.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.google.api.services.androidpublisher.model.ProductPurchase; +import com.sonic.lion.channel.config.StripeConfig; +import com.sonic.lion.dao.PayCallChannelRecordDao; +import com.sonic.lion.domain.bo.WebhookBo; +import com.sonic.lion.domain.entity.PayCallChannelRecord; +import com.sonic.lion.domain.entity.UserSubscriptionNotify; +import com.sonic.lion.domain.enums.CallChannelStatus; +import com.sonic.lion.domain.input.ChannelWebhookInput; +import com.sonic.lion.domain.req.GoogleUploadReceiptReq; +import com.sonic.lion.domain.req.GoogleUploadReceiptReqV2; +import com.sonic.lion.enums.*; +import com.sonic.lion.service.*; +import com.sonic.lion.utils.AppleWebhookVerifier; +import com.sonic.lion.utils.JwtUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.UUID; + +@Slf4j +@Service +public class WebhookHandlerServiceImpl implements WebhookHandlerService { + + @Autowired + private IapV2Service iapV2Service; + @Autowired + private GoogleService googleService; + @Autowired + private UserSubscriptionService userSubscriptionService; + @Autowired + private PayCallChannelRecordDao payCallChannelRecordDao; + @Autowired + private TradeHandler tradeHandler; + @Autowired + private GoogleUploadReceiptService googleUploadReceiptService; + @Autowired + private AppleWebhookVerifier appleWebhookVerifier; + @Autowired + private StripeServiceImpl stripeService; + @Autowired + private StripeConfig stripeConfig; + @Autowired + private StripeSubscribeService stripeSubscribeService; + + @Override + public void googleHandler(GoogleUploadReceiptReqV2 req) { + boolean exitsReceipt = googleUploadReceiptService.countRecepit(req.getReceipt()); + BizResultCode.EXISTS_ERROR.check(exitsReceipt, "1019", "duplicate receipt"); + try { + ProductPurchase productPurchase = googleService.googleCheck(req.getProductId(), req.getReceipt()); + GoogleUploadReceiptReq googleUploadReceiptReq = new GoogleUploadReceiptReq(); + googleUploadReceiptReq.setReceipt(req.getReceipt()); + googleUploadReceiptReq.setProductId(req.getProductId()); + googleUploadReceiptReq.setTradeNo(productPurchase.getObfuscatedExternalProfileId()); + googleService.uploadReceipt(googleUploadReceiptReq); + } catch (Exception e) { + log.error("上传Google上传收据异常:", e); + throw new RuntimeException(e); + } + } + + @Override + public void appleHandler(HttpServletRequest request) throws Exception { + String payload = getPayload(request); + log.info("===> appleV2 webhook payload: {} ", payload); + //入参验签处理 + boolean bl = appleWebhookVerifier.handleAppleWebhook(JSONObject.parseObject(payload).getString("signedPayload")); + ToastResultCode.SYSTEM_EXCEPTION.check(!bl, "", "sign verifier error"); + //解出JWT中的body内容 + String jwtBody = JwtUtils.getJwtBody(payload); + JSONObject jwtBodyJson = JSON.parseObject(jwtBody); + JSONObject jwtBodyDataJson = jwtBodyJson.getJSONObject("data"); + //解析出交易原数据 + JSONObject signedTransactionInfoJson = JSON.parseObject(JwtUtils.getJwtBody(jwtBodyDataJson.getString("signedTransactionInfo"))); + //设置到data对象中去 + jwtBodyDataJson.put("transactionInfo", signedTransactionInfoJson); + //获取自动续订的签名数据 + String signedRenewalInfoBody = JwtUtils.getJwtBody(jwtBodyDataJson.getString("signedRenewalInfo")); + //获取自动续订商品ID + String autoRenewProductId = null; + if(StringUtils.isNoneBlank(signedRenewalInfoBody)) { + JSONObject signedRenewalInfoJson = JSON.parseObject(signedRenewalInfoBody); + //设置到data对象中去 + jwtBodyDataJson.put("renewalInfo", signedRenewalInfoJson); + autoRenewProductId = signedRenewalInfoJson.getString("autoRenewProductId"); + } + //移除掉两个签名数据 + jwtBodyDataJson.remove("signedTransactionInfo"); + jwtBodyDataJson.remove("signedRenewalInfo"); + //将数据放回去 + jwtBodyJson.put("data", jwtBodyDataJson); + //获取通知类型,自动续订的产品id + String notificationType = jwtBodyJson.getString("notificationType"); + + log.info("===> appleV2 webhook json: {}, {}, {}", autoRenewProductId, notificationType, jwtBodyJson.toJSONString()); + + Long notifyId = userSubscriptionService.addSubscriptionNotifyV2("", UUID.randomUUID().toString().replace("-", ""), jwtBodyJson.toJSONString(), payload, notificationType, UserSubscriptionNotify.Platform.IOS); + try { + //调用v2版本的方法处理回调 + iapV2Service.handSubscribeWebhook(notifyId, jwtBodyJson.toJSONString()); + } catch (Exception e) { + log.error("===> appleV2 webhook handler error notifyId1 : " + notifyId, e); + } + } + + @Override + public void stripePaymentHandler(String signature, String payload) { + ChannelWebhookInput input = ChannelWebhookInput.builder() + .signature(signature) + .payload(payload) + .build(); + //验签 + WebhookBo webhookBo = stripeService.webhookBeforeHandler(input, stripeConfig.getPaymentWebhookSecret()); + //处理交易 + String batchId = webhookBo == null ? null : webhookBo.getId(); + if (batchId == null) { + log.error("===> stripePaymentHandler 不能在webhook中获取batchId"); + return; + } + PayCallChannelRecord record = payCallChannelRecordDao.selectByBatchId(batchId); + if (record == null) { + log.error("===> stripePaymentHandler 未找到调用记录:{}", batchId); + return; + } + if (record.getStatus() == CallChannelStatus.SUCC) { + log.info("===> stripePaymentHandler 渠道调用记录已是成功状态"); + return; + } + //处理账户 加款 流程 + tradeHandler.syncOrder(record); + log.info("===> stripePaymentHandler end"); + } + + @Override + public void stripePayoutHandler(String signature, String payload) { + ChannelWebhookInput input = ChannelWebhookInput.builder() + .signature(signature) + .payload(payload) + .build(); + //验签 + WebhookBo webhookBo = stripeService.webhookBeforeHandler(input, stripeConfig.getPayoutWebhookSecret()); + String batchId = webhookBo == null ? null : webhookBo.getId(); + if (batchId == null) { + log.error("===> stripePayoutHandler 不能在webhook中获取batchId"); + return; + } + PayCallChannelRecord record = payCallChannelRecordDao.selectByBatchId(batchId); + if (record == null) { + log.error("===> stripePayoutHandler 未找到调用记录:{}", batchId); + return; + } + if (record.getStatus() == CallChannelStatus.SUCC) { + log.info("===> stripePayoutHandler 渠道调用记录已是成功状态"); + return; + } + //处理账户 扣款 流程 + tradeHandler.syncOrder(record); + log.info("===> stripePayoutHandler end"); + } + + @Override + public void stripeSubscriptionHandler(String signature, String payload) throws Exception { + ChannelWebhookInput input = ChannelWebhookInput.builder() + .signature(signature) + .payload(payload) + .build(); + //验签 + WebhookBo webhookBo = stripeService.webhookBeforeHandler(input, stripeConfig.getSubscriptionWebhookSecret()); + log.error("===> stripeSubscriptionHandler webhookBo : {}", webhookBo); + //处理交易 + String batchId = webhookBo == null ? null : webhookBo.getId(); + if (batchId == null) { + log.error("===> stripeSubscriptionHandler 不能在webhook中获取batchId"); + return; + } + //订阅通知消息入库 + Long notifyId = userSubscriptionService.addSubscriptionNotify(webhookBo.getId(), webhookBo.getCustomerId(), payload, webhookBo.getEventType(), UserSubscriptionNotify.Platform.STRIPE); + //处理订阅表的基础数据处理 + stripeSubscribeService.subscriptionHandler(notifyId, webhookBo); + log.info("===> stripeSubscriptionHandler end"); + } + + @Override + public void stripeDisputeHandler(String signature, String payload) throws Exception { + ChannelWebhookInput input = ChannelWebhookInput.builder() + .signature(signature) + .payload(payload) + .build(); + //验签 + WebhookBo webhookBo = stripeService.webhookBeforeHandler(input, stripeConfig.getDisputeWebhookSecret()); + //处理交易 + String batchId = webhookBo == null ? null : webhookBo.getId(); + if (batchId == null) { + log.error("===> stripeDisputeHandler 不能在webhook中获取batchId"); + return; + } + PayCallChannelRecord record = payCallChannelRecordDao.selectByBatchId(batchId); + if (record == null) { + log.error("===> stripeDisputeHandler 未找到调用记录:{}", batchId); + return; + } + if (record.getStatus() == CallChannelStatus.SUCC) { + log.info("===> stripeDisputeHandler 渠道调用记录已是成功状态"); + return; + } + //处理账户 争议 流程 FIXME + + log.info("===> stripeDisputeHandler end"); + } + + + /** + * 获取入参数据 + * + * @param request + * @return + * @throws Exception + */ + private String getPayload(HttpServletRequest request) throws Exception { + String body; + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader bufferedReader = null; + try { + InputStream inputStream = request.getInputStream(); + if (inputStream != null) { + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + char[] charBuffer = new char[128]; + int bytesRead = -1; + while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { + stringBuilder.append(charBuffer, 0, bytesRead); + } + } + } catch (IOException ex) { + throw ex; + } finally { + if (bufferedReader != null) { + try { + bufferedReader.close(); + } catch (IOException ex) { + throw ex; + } + } + } + body = stringBuilder.toString(); + return body; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/WithdrawServiceImpl.java b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/WithdrawServiceImpl.java new file mode 100644 index 0000000..af225c5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/service/impl/WithdrawServiceImpl.java @@ -0,0 +1,335 @@ +package com.sonic.lion.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.sonic.lion.enums.*; +import com.sonic.lion.utils.KeyGenerator; +import com.sonic.lion.dao.*; +import com.sonic.lion.domain.bo.WithdrawTradeExtendBo; +import com.sonic.lion.domain.entity.*; +import com.sonic.lion.domain.enums.CallChannelStatus; +import com.sonic.lion.domain.input.BuffChangeInput; +import com.sonic.lion.domain.input.ChannelPayoutInput; +import com.sonic.lion.domain.input.WithdrawFeeInput; +import com.sonic.lion.domain.output.ChannelPayoutOutput; +import com.sonic.lion.service.*; +import com.sonic.lion.enums.ToastResultCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +import static com.sonic.lion.enums.PayGenrtatorCodeType.TRADE; + +@Slf4j +@Service +public class WithdrawServiceImpl implements WithdrawService { + + @Value("${site.type}") + private String siteType; + @Autowired + private PayCallChannelService payCallChannelService; + + @Autowired + private AccountBuffService accountBuffService; + + @Autowired + private FreeWithdrawConfigService freeWithdrawConfigService; + + @Autowired + private PayTradeDao payTradeDao; + + @Autowired + private PayTradeService payTradeService; + + @Autowired + private AccountBuffBillService accountBuffBillService; + + @Autowired + private AccountBuffDao accountBuffDao; + + @Autowired + private FreeWithdrawFeeBillDao freeWithdrawFeeBillDao; + + @Autowired + private ProcessingWithdrawReviewDao processingWithdrawReviewDao; + @Autowired + private ProcessingWithdrawDao processingWithdrawDao; + + @Autowired + private PayAccountFundThirdService payAccountFundThirdService; + + @Override + public void checkAmount(PayChannel payChannel, Long amount, Long userId) { + if (PayChannel.STRIPE.equals(payChannel)) { + ToastResultCode.AMOUNT_LESS_THAN_N.check(amount < 100, "1.00"); + } + } + + /** + * 提现审核 提现先审核一天时间 + * 后定时任务 再 进行提现。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void withdrawReview(WithdrawRequest input) { + String tradeNo = KeyGenerator.instance().generatorUniqueKey(TRADE); + String outTradeNo = tradeNo; + + + BizType bizType = BizType.WITHDRAW; + PaymentType paymentType = PaymentType.BALANCE; + //入参金额为扣款金额,真实能到用户账上的金额需从里面减去手续费 + + PayTrade payTrade = PayTrade.builder() + .id(IdWorker.getId()) + .platform(input.getPlatform()) + .tradeNo(tradeNo) + .outTradeNo(outTradeNo) + .srcAccountId(input.getSrcAccountId()) + .srcAccountName(input.getSrcAccountName()) + .desAccountNo(input.getDesAccountNo()) + .desAccountName(input.getDesAccountName()) + .payChannel(PayChannel.get(input.getPayChannel())) + .channelName(PayChannel.get(input.getPayChannel()).getDesc()) + .bizType(bizType) + .status(TradeStatus.PROCESSING) + .name(input.getName()) + .occurAmount(input.getAmount()) + .paymentType(paymentType) + .coinType(input.getCoinType()) + .createTime(input.getCreateTime()) + .build(); + + // 计算手续费 + WithdrawFeeInput fee = freeWithdrawConfigService.calculateWithdrawFee(payTrade); + payTrade.setAmount(input.getAmount() - fee.getWithdrawFee()); + payTrade.setFee(fee.getWithdrawFee()); + payTrade.setThirdFee(fee.getThirdFee()); + payTrade.setPlatformFee(fee.getPlatformFee()); + + if(fee.getFreeWithdrawBillId() != null || StringUtils.isNotEmpty(input.getCurrencyType())) { + WithdrawTradeExtendBo bo = new WithdrawTradeExtendBo(); + //如果有免手续费记录 就写入记录ID + if (fee.getFreeWithdrawBillId() != null) { + bo.setFreeWithdrawBillId(fee.getFreeWithdrawBillId()); + } + //设置扩展字段,设置目标用户的提现货币类型 + if(StringUtils.isNotEmpty(input.getCurrencyType())) { + bo.setCurrencyType(input.getCurrencyType()); + } + payTrade.setExtend(JSONObject.toJSONString(bo)); + } + + //写入流水表 状态为 处理中。 + AccountBuff accountBuff = accountBuffService.getByUid(input.getSrcAccountId()); + Long billId = accountBuffBillService.insertBillWhenWithdraw(payTrade, accountBuff); + //关联下流水表 + payTrade.setOutTradeNoRelationNo(billId.toString()); + payTradeDao.insert(payTrade); + + int n = accountBuffDao.decWithdrawableIncome(accountBuff.getId(), payTrade.getOccurAmount()); + + ToastResultCode.INSUFFICIENT_BALANCE.check(n != 1); + //更新 提现在途资金 + accountBuffService.addWithdrawOnGoing(accountBuff.getId(), payTrade.getOccurAmount()); + + ProcessingWithdrawReview processingWithdraw = ProcessingWithdrawReview.builder() + .tradeId(payTrade.getId()) + .tradeNo(tradeNo) + .createTime(LocalDateTime.now()) + .payChannel(payTrade.getPayChannel()) + .build(); + processingWithdrawReviewDao.insert(processingWithdraw); + } + + /** + * 过了审核期 的进行 提现操作。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void handWithdrawWhenReviewTimeOut(PayTrade payTrade) { + //调用 三方渠道进行提现。 去掉了之前的发送MQ逻辑 简化代码。因为这里是定时任务处理 没有客户直接请求 不同步返回数据。 不影响用户体验。 + PayTrade db = payTradeService.getByTradeNo(payTrade.getTradeNo()); + ChannelPayoutInput input = ChannelPayoutInput.builder() + .amount(payTrade.getAmount()) + .thirdFee(payTrade.getThirdFee()) + .desAccountNo(payTrade.getDesAccountNo()) + //提现渠道暂时写死 目前只支持PAYPAL + //增加了新的提现渠道 + .payChannel(db.getPayChannel()) + .remark(payTrade.getRemark()) + .tradeNo(payTrade.getTradeNo()) + .payTrade(payTrade) + .bizType(payTrade.getBizType()) + .userId(payTrade.getSrcAccountId()) + .tradeId(payTrade.getId()) + .build(); + //根据渠道调用结果 更新状态 或 回滚资金 + try { + ChannelPayoutOutput payoutOutput = payCallChannelService.callPayout(input); + log.info("提现渠道最终状态: {}", payoutOutput); + if (CallChannelStatus.FAIL.equals(payoutOutput.getStatus())) { + log.info("提现调用失败开始回滚资金, tradeNo: {}", input.getTradeNo()); + withdrawFail(input.getTradeNo(), payTrade.getBizType(), payoutOutput.getErrorMessage()); + + } else if (CallChannelStatus.PROCESSING.equals(payoutOutput.getStatus())) { + accountBuffBillService.updateWithdrawStatus(Long.valueOf(payTrade.getOutTradeNoRelationNo()), AccountBuffBill.WithdrawStatus.WITHDRAW_ING, ""); + } else if (CallChannelStatus.SUCC.equals(payoutOutput.getStatus())) { + + AccountBuff accountBuff = accountBuffService.getByUid(payTrade.getSrcAccountId()); + accountBuffService.decWithdrawOnGoing(accountBuff.getId(), payTrade.getOccurAmount()); + accountBuffBillService.updateWithdrawStatus(Long.valueOf(payTrade.getOutTradeNoRelationNo()), AccountBuffBill.WithdrawStatus.WITHDRAW_SUCCESS, ""); + + accountBuffBillService.updateWithdrawBillAmount(Long.valueOf(payTrade.getOutTradeNoRelationNo()), payTrade.getOccurAmount()); + //提现成功,发送系统消息 + } + log.info("提现逻辑执行完成, tradeNo: {}", input.getTradeNo()); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void withdrawFail(@NonNull String tradeNo, BizType bizType, String message) { + PayTrade payTrade = payTradeService.getByTradeNo(tradeNo); + ToastResultCode.DATA_NOT_EXITS.check(payTrade == null); + ToastResultCode.UNSUPPORTED_BUSINESS_TYPE.check(payTrade.getBizType() != BizType.WITHDRAW); + ToastResultCode.DATA_STATUS_INCORRECT.check(payTrade.getStatus() != TradeStatus.PROCESSING); + //确认支付渠道提现状态 + PayCallChannelRecord payCallChannelRecord = payCallChannelService.getByTradeNoAndBizTypeLast(payTrade.getTradeNo(), payTrade.getBizType()); + ToastResultCode.DATA_STATUS_INCORRECT.check(payCallChannelRecord != null && payCallChannelRecord.getStatus() != CallChannelStatus.FAIL); + if (!BizType.WITHDRAW.equals(bizType)) { + return; + } + AccountBuff accountBuff = accountBuffService.getByUid(payTrade.getSrcAccountId()); + accountBuffService.withdrawOnGoingRollback(accountBuff.getId(), payTrade.getOccurAmount()); + //更新状态为 关闭 + accountBuffBillService.updateWithdrawStatus(Long.valueOf(payTrade.getOutTradeNoRelationNo()), AccountBuffBill.WithdrawStatus.WITHDRAW_FAIL, message); + //提现失败,插入一条返还的流水 + accountBuffBillService.insertBillWhenWithdrawFail(Long.valueOf(payTrade.getOutTradeNoRelationNo()), accountBuff); + //更新支付交易状态为CLOSED + payTradeService.updateStatus(payTrade.getId(), TradeStatus.CLOSED, TradeStatus.PROCESSING); + try { + //如果之前免手续费 , 回滚手续费记录表 + if (StringUtils.isNotEmpty(payTrade.getExtend())) { + //解析出实体对象 + WithdrawTradeExtendBo bo = JSONObject.parseObject(payTrade.getExtend(), WithdrawTradeExtendBo.class); + if(bo.getFreeWithdrawBillId() != null) { + //构造更新数据 + FreeWithdrawFeeBill updater = FreeWithdrawFeeBill.builder().id(bo.getFreeWithdrawBillId()).deleted(Boolean.TRUE).build(); + freeWithdrawFeeBillDao.updateById(updater); + } + } + } catch (Exception e) { + //因为有老数据,所以这里先吃掉异常进行处理(主要目的是为了兼容老数据) + log.error("===> withdrawFail handler FreeWithdrawFeeBill error : ", e); + if (StringUtils.isNotEmpty(payTrade.getExtend())) { + //构造更新数据(当老数据在解析json时出现异常,所以在这里直接拿扩展字段值进行处理) + FreeWithdrawFeeBill updater = FreeWithdrawFeeBill.builder().id(Long.valueOf(payTrade.getExtend())).deleted(Boolean.TRUE).build(); + freeWithdrawFeeBillDao.updateById(updater); + } + } + //提现失败,发送系统消息 + } + + + /** + * 提现成功 + * + * @param tradeNo + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void withdrawSuccess(@NonNull String tradeNo) { + PayTrade payTrade = payTradeService.getByTradeNo(tradeNo); + if (!BizType.WITHDRAW.equals(payTrade.getBizType())) { + return; + } + String outTradeNoRelationNo = payTrade.getOutTradeNoRelationNo(); + Long billId = Long.valueOf(outTradeNoRelationNo); + accountBuffBillService.updateWithdrawStatus(billId, AccountBuffBill.WithdrawStatus.WITHDRAW_SUCCESS, ""); + payTradeService.updateStatus(payTrade.getId(), TradeStatus.FINISHED, TradeStatus.PROCESSING); + AccountBuff accountBuff = accountBuffService.getByUid(payTrade.getSrcAccountId()); + accountBuffService.decWithdrawOnGoing(accountBuff.getId(), payTrade.getOccurAmount()); + accountBuffBillService.updateWithdrawBillAmount(billId, payTrade.getOccurAmount()); + //提现成功,发送系统消息 + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void mockWithdrawToFinalState(String tradeNo, ChannelPayoutOutput payoutOutput) { + PayTrade db = payTradeService.getByTradeNo(tradeNo); + //找不到数据,抛出异常 + ToastResultCode.DATA_NOT_EXITS.check(db == null || BizType.WITHDRAW != db.getBizType()); + //判断交易状态,只能为处理中、处理成功的状态下才允许进行处理。如果状态为成功但入参状态不为失败时也不能进行处理 + if((TradeStatus.PROCESSING != db.getStatus() && TradeStatus.FINISHED != db.getStatus()) || (CallChannelStatus.FAIL != payoutOutput.getStatus() && TradeStatus.FINISHED == db.getStatus())) { + ToastResultCode.PAY_TRADE_STATUS_CHANGE.check(true); + } + log.info("提现渠道最终状态: {}", payoutOutput); + if (CallChannelStatus.FAIL.equals(payoutOutput.getStatus())) { + //这里分为处理中的失败和 成功后的失败,成功后的失败就是走补偿的逻辑 + if(TradeStatus.PROCESSING == db.getStatus()) { + //重新从三方渠道获取详情,并更新渠道调用表的状态为失败 + PayCallChannelRecord record = payCallChannelService.getByTradeNoAndBizTypeLast(db.getTradeNo(), db.getBizType()); + if(record == null) { + log.info("===> PayCallChannelRecord not found"); + return; + } + //先将渠道表的处理状态更新成失败 + payCallChannelService.updateStatus(record.getId(), CallChannelStatus.FAIL, CallChannelStatus.PROCESSING); + //走失败的逻辑 + withdrawFail(tradeNo, db.getBizType(), payoutOutput.getErrorMessage()); + } else if(TradeStatus.FINISHED == db.getStatus()) { + //重新从三方渠道获取详情,并更新渠道调用表的状态为失败 + PayCallChannelRecord record = payCallChannelService.getByTradeNoAndBizTypeLast(db.getTradeNo(), db.getBizType()); + //渠道调用记录不存在 或者 状态不为已成功 直接快速返回不处理 + if(record == null || CallChannelStatus.SUCC != record.getStatus()) { + log.info("===> PayCallChannelRecord not found"); + return; + } + //渠道调用表 修改成失败 + payCallChannelService.updateStatus(record.getId(), CallChannelStatus.FAIL, CallChannelStatus.SUCC); + //交易表 修改成失败 + payTradeService.updateStatus(db.getId(), TradeStatus.CLOSED, TradeStatus.FINISHED); + + BuffChangeInput input = new BuffChangeInput(); + input.setUid(db.getSrcAccountId()); + input.setBuff(db.getOccurAmount()); + input.setTradeNo(db.getTradeNo()); + input.setErrorMessage(payoutOutput.getErrorMessage()); + //回滚资金,并将交易号对应的流水状态更新为失败 + accountBuffService.addWithdrawableIncome(input); + + AccountBuff accountBuff = accountBuffService.getByUid(db.getSrcAccountId()); + //提现失败,再插入一条返还的流水 + accountBuffBillService.insertBillWhenWithdrawFail(Long.valueOf(db.getOutTradeNoRelationNo()), accountBuff); + } + } else if (CallChannelStatus.SUCC.equals(payoutOutput.getStatus())) { + AccountBuff accountBuff = accountBuffService.getByUid(db.getSrcAccountId()); + accountBuffService.decWithdrawOnGoing(accountBuff.getId(), db.getOccurAmount()); + accountBuffBillService.updateWithdrawStatus(Long.valueOf(db.getOutTradeNoRelationNo()), AccountBuffBill.WithdrawStatus.WITHDRAW_SUCCESS, ""); + + accountBuffBillService.updateWithdrawBillAmount(Long.valueOf(db.getOutTradeNoRelationNo()), db.getOccurAmount()); + //提现成功,发送系统消息 + } + log.info("提现逻辑执行完成, tradeNo: {}", db.getTradeNo()); + //删除处理中的数据 + ProcessingWithdraw processingWithdraw = processingWithdrawDao.selectOne(Wrappers.lambdaQuery() + .eq(ProcessingWithdraw::getTradeNo, db.getTradeNo())); + if(processingWithdraw != null) { + processingWithdrawDao.deleteById(processingWithdraw.getId()); + } + } + + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/sub/SubscribeAsyncGoogleSubPub.java b/sonic-lion/server/src/main/java/com/sonic/lion/sub/SubscribeAsyncGoogleSubPub.java new file mode 100644 index 0000000..1242082 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/sub/SubscribeAsyncGoogleSubPub.java @@ -0,0 +1,166 @@ +package com.sonic.lion.sub; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.google.api.services.androidpublisher.model.ProductPurchase; +import com.google.cloud.pubsub.v1.AckReplyConsumer; +import com.google.cloud.pubsub.v1.MessageReceiver; +import com.google.cloud.pubsub.v1.Subscriber; +import com.google.pubsub.v1.ProjectSubscriptionName; +import com.google.pubsub.v1.PubsubMessage; +import com.sonic.common.utils.LogUtils; +import com.sonic.lion.domain.entity.UserSubscriptionNotify; +import com.sonic.lion.domain.req.GoogleUploadReceiptReq; +import com.sonic.lion.service.GoogleService; +import com.sonic.lion.service.GoogleUploadReceiptService; +import com.sonic.lion.service.UserSubscriptionService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * https://developer.android.com/google/play/billing/rtdn-reference?hl=zh-cn + */ +@Component +@Slf4j +public class SubscribeAsyncGoogleSubPub implements ApplicationRunner, DisposableBean { + + @Value("${google.projectId}") + private String projectId; + @Value("${google.subscriptionId}") + private String subscriptionId; + + @Autowired + private UserSubscriptionService userSubscriptionService; + + @Autowired + private GoogleService googleService; + + @Autowired + private GoogleUploadReceiptService googleUploadReceiptService; + + private Subscriber subscriber = null; + + @Value("${spring.profiles.active}") + private String env; + + @Override + public void run(ApplicationArguments args) throws Exception { + if (!"product".equals(env)) { + log.info("SubscribeAsyncGoogleSubPub not running env is not product !!!{}", env); + return; + } + log.info("SubscribeAsyncGoogleSubPub was started!"); + + Map notificationMap = new HashMap<>(); + notificationMap.put(1, "SUBSCRIPTION_RECOVERED"); + notificationMap.put(2, "SUBSCRIPTION_RENEWED"); + notificationMap.put(3, "SUBSCRIPTION_CANCELED"); + notificationMap.put(4, "SUBSCRIPTION_PURCHASED"); + notificationMap.put(5, "SUBSCRIPTION_ON_HOLD"); + notificationMap.put(6, "SUBSCRIPTION_IN_GRACE_PERIOD"); + notificationMap.put(7, "SUBSCRIPTION_RESTARTED"); + notificationMap.put(8, "SUBSCRIPTION_PRICE_CHANGE_CONFIRMED"); + notificationMap.put(9, "SUBSCRIPTION_DEFERRED"); + notificationMap.put(10, "SUBSCRIPTION_PAUSED"); + notificationMap.put(11, "SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED"); + notificationMap.put(12, "SUBSCRIPTION_REVOKED"); + notificationMap.put(13, "SUBSCRIPTION_EXPIRED"); + + try { + start(notificationMap); + } catch (Exception e) { + log.error("SubscribeAsyncGoogleSubPub error!", e); + } + } + + public void start(Map NotificationMap3) { + subscribeAsync(projectId, subscriptionId, NotificationMap3); + } + + + /** + * https://developer.android.com/google/play/billing/rtdn-reference + * + * @param projectId + * @param subscriptionId + * @param NotificationMap3 + */ + public void subscribeAsync(String projectId, String subscriptionId, Map NotificationMap3) { + ProjectSubscriptionName subscriptionName = + ProjectSubscriptionName.of(projectId, subscriptionId); + MessageReceiver receiver = + (PubsubMessage message, AckReplyConsumer consumer) -> { + try { + LogUtils.setTraceId(); + // Handle incoming message, then ack the received message. + String messageId = message.getMessageId(); + String messageBody = message.getData().toStringUtf8(); + log.info("===> subscribeAsync messageId:{} ,messageBody:{} ", messageId, messageBody); + JSONObject jsonObject = JSON.parseObject(messageBody); + JSONObject subscriptionNotification = jsonObject.getJSONObject("subscriptionNotification"); + if (subscriptionNotification != null) { + Long notifyId = 0L; + Integer type = subscriptionNotification.getInteger("notificationType"); + String typeDesc = NotificationMap3.get(type); + try { + notifyId = userSubscriptionService.addSubscriptionNotify("", messageId, messageBody, typeDesc, UserSubscriptionNotify.Platform.GOOGLE); + } catch (org.springframework.dao.DuplicateKeyException e) { + log.info("google 订阅消息已经被处理 messageId:{}", messageId); + } + try { + googleService.handSubscribeWebhook(notifyId, typeDesc, messageBody); + } catch (Exception e) { + log.info("google 处理失败 notifyId:{}", notifyId); + log.error("google 处理失败", e); + userSubscriptionService.failNotify(notifyId); + } + } else { + JSONObject oneTimeProductNotification = jsonObject.getJSONObject("oneTimeProductNotification"); + if (oneTimeProductNotification != null) { + String token = oneTimeProductNotification.getString("purchaseToken"); + String productId = oneTimeProductNotification.getString("sku"); + boolean exitsRecepit = googleUploadReceiptService.countRecepit(token); + if (!exitsRecepit) { + log.info("发现丢单 处理中...{} ,{}", productId, token); + //处理丢单 + GoogleUploadReceiptReq googleUploadReceiptReq = new GoogleUploadReceiptReq(); + googleUploadReceiptReq.setProductId(productId); + googleUploadReceiptReq.setReceipt(token); + try { + ProductPurchase productPurchase = googleService.googleCheck(productId, token); + if(StringUtils.isNotBlank(productPurchase.getObfuscatedExternalProfileId())){ + log.info("开始处理google 丢单:{}",productPurchase.getObfuscatedExternalProfileId()); + googleUploadReceiptReq.setTradeNo(productPurchase.getObfuscatedExternalProfileId()); + googleService.uploadReceipt(googleUploadReceiptReq); + } + } catch (Exception e) { + e.printStackTrace(); + log.error("google 丢单补偿失败",e); + } + } + } + } + consumer.ack(); + } finally { + LogUtils.removeTraceId(); + } + }; + subscriber = Subscriber.newBuilder(subscriptionName, receiver).build(); + // Start the subscriber. + subscriber.startAsync().awaitRunning(); + } + + @Override + public void destroy() throws Exception { + subscriber.stopAsync(); + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/test/StripeConnectedAccountPayoutTest.java b/sonic-lion/server/src/main/java/com/sonic/lion/test/StripeConnectedAccountPayoutTest.java new file mode 100644 index 0000000..f37c392 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/test/StripeConnectedAccountPayoutTest.java @@ -0,0 +1,136 @@ +package com.sonic.lion.test; + +import com.stripe.Stripe; +import com.stripe.exception.StripeException; +import com.stripe.model.Account; +import com.stripe.model.BankAccount; +import com.stripe.model.Payout; +import com.stripe.model.Transfer; +import com.stripe.net.RequestOptions; + +import java.util.HashMap; +import java.util.Map; + +/** + * Stripe Connected Accounts 测试流程示例 + * + * 这个类演示了使用 Stripe Java SDK 的完整测试流程: + * 1. 创建一个 Express 类型的连接账户(Connected Account)。 + * 2. 为连接账户添加一个测试银行账户(External Account)。 + * 3. 从平台账户向连接账户转移资金(Transfer)。 + * 4. 为连接账户手动创建提现(Payout)到银行账户。 + * + * 注意: + * - 使用测试密钥(sk_test_...)在测试模式下运行,不会实际转账。 + * - 在生产环境中,需要处理 KYC/Onboarding(例如,通过 Account Links API 引导用户输入真实信息)。 + * - 错误处理是基本的;生产代码应更全面。 + * - 确保已添加 stripe-java 依赖(Maven/Gradle)。 + * + * 全球付款测试 + * https://docs.stripe.com/global-payouts + * + */ +public class StripeConnectedAccountPayoutTest { + + private static final String STRIPE_API_KEY = "sk_test_51RoLASRHHBJ3NEeaS8VxcRsjapRASYt4lM4HHn22ZKbHMBzwce0kgJPECtoRcntzG4fktfjoDvHE4Pmx8Vpy8bLU008zUxKdxB"; // 替换为你的 Stripe 测试密钥 + + public static void main(String[] args) { + // 设置 Stripe API 密钥 + Stripe.apiKey = STRIPE_API_KEY; + + try { +// // 步骤 1: 创建 Express 连接账户 +// Account account = createConnectedAccount(); +// System.out.println("创建连接账户: ID = " + account.getId()); +// +// // 步骤 2: 为连接账户添加测试银行账户 +// BankAccount bankAccount = addBankAccount(account.getId()); +// System.out.println("添加银行账户: ID = " + bankAccount.getId()); + + Account account = new Account(); + account.setId("acct_1S7vSrEvZDpJJVSo"); + //ba_1S7vSvEvZDpJJVSok76s8rog + + // 步骤 3: 从平台账户向连接账户转移资金 + Transfer transfer = createTransfer(account.getId(), 1000L); // 1000 美分 = 10 USD + System.out.println("转移资金: Transfer ID = " + transfer.getId()); + + // 步骤 4: 为连接账户创建手动 Payout + Payout payout = createPayout(account.getId(), 1000L); // 提现 10 USD + System.out.println("创建 Payout: Payout ID = " + payout.getId()); + + System.out.println("测试流程完成!请在 Stripe Dashboard 检查结果。"); + + } catch (StripeException e) { + System.err.println("Stripe API 错误: " + e.getMessage()); + System.err.println("Request ID: " + e.getRequestId()); // 用于在 Stripe Dashboard 追踪 + e.printStackTrace(); + } catch (Exception e) { + System.err.println("其他错误: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 创建 Express 连接账户 + */ + private static Account createConnectedAccount() throws StripeException { + Map params = new HashMap<>(); + params.put("type", "express"); + params.put("country", "US"); + params.put("email", "testuser@example.com"); + Map capabilities = new HashMap<>(); + capabilities.put("card_payments", new HashMap() {{ + put("requested", true); + }}); + capabilities.put("transfers", new HashMap() {{ + put("requested", true); + }}); + params.put("capabilities", capabilities); + return Account.create(params); + } + + /** + * 为连接账户添加测试银行账户 + * 修复:将银行账户参数嵌套在 external_account 键下,符合 stripe-java 20.108.0 要求 + */ + private static BankAccount addBankAccount(String accountId) throws StripeException { + RequestOptions options = RequestOptions.builder().setStripeAccount(accountId).build(); + Map params = new HashMap<>(); + Map externalAccount = new HashMap<>(); + externalAccount.put("object", "bank_account"); + externalAccount.put("country", "US"); + externalAccount.put("currency", "usd"); + externalAccount.put("account_holder_name", "Test User"); + externalAccount.put("account_holder_type", "individual"); + externalAccount.put("routing_number", "110000000"); // Stripe 测试路由号 + externalAccount.put("account_number", "000123456789"); // Stripe 测试账号 + params.put("external_account", externalAccount); // 关键:嵌套在 external_account + return (BankAccount) Account.retrieve(accountId).getExternalAccounts().create(params, options); + } + + /** + * 从平台账户向连接账户转移资金 + */ + private static Transfer createTransfer(String accountId, Long amount) throws StripeException { + Map params = new HashMap<>(); + params.put("amount", amount); + params.put("currency", "usd"); + params.put("destination", accountId); + params.put("description", "测试转移到连接账户"); + return Transfer.create(params); + } + + /** + * 为连接账户创建手动 Payout + */ + private static Payout createPayout(String accountId, Long amount) throws StripeException { + RequestOptions options = RequestOptions.builder().setStripeAccount(accountId).build(); + Map params = new HashMap<>(); + params.put("amount", amount); + params.put("currency", "usd"); + params.put("method", "standard"); + params.put("description", "测试提现"); + return Payout.create(params, options); + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/AbstractKeyGenerator.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/AbstractKeyGenerator.java new file mode 100644 index 0000000..78aa4a4 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/AbstractKeyGenerator.java @@ -0,0 +1,105 @@ +package com.sonic.lion.utils; + +import com.sonic.common.exception.SysException; +import com.sonic.common.exception.SysExceptionUtils; +import org.apache.log4j.Logger; + +import java.net.InetAddress; +import java.text.NumberFormat; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class AbstractKeyGenerator { + private static final Logger LOGGER = Logger.getLogger(AbstractKeyGenerator.class); + private static final int SEQ_MIN = 1; + private static final int SEQ_MAX = 999; + private static final int SEQ_DIGITS = 3; + private static final AtomicInteger SEQ_ATOMIC_INTEGER = new AtomicInteger(1); + private static final char IP_SPACER = '.'; + private static final String IP_AFTER_TWO; + private static final int IP_DIGITS = 2; + private static final int IP_MOD = 99; + private static final String PATTERN = "yyyyMMddHHmmssSSS"; + private static final int ORDER_NO_MAX_SIZE = 29; + + public AbstractKeyGenerator() { + } + + public String generatorUniqueKey(String businessCode) { + StringBuffer buffer = new StringBuffer(); + String date = DateConvertUtils.getNow(PATTERN); + buffer.append(this.customKey()).append(businessCode).append(date).append(IP_AFTER_TWO).append(formatNumber((long)getSeq(), SEQ_DIGITS)); + + SysExceptionUtils.check(buffer.length() > ORDER_NO_MAX_SIZE,"", "tradeNo to long, tradeNo->" + buffer.toString()); + return buffer.toString(); + } + + public String generatorOrderKey(Boolean isShort) { + StringBuffer buffer = new StringBuffer(); + return this.generatorOrderKey(isShort, buffer, ""); + } + + public String generatorOrderKey(Boolean isShort, String businessCode) { + StringBuffer buffer = new StringBuffer(); + return this.generatorOrderKey(isShort, buffer, businessCode); + } + + public String generatorOrderKey(Boolean isShort, String prefix, String businessCode) { + StringBuffer buffer = new StringBuffer(prefix); + return this.generatorOrderKey(isShort, buffer, businessCode); + } + + public String generatorOrderKey(Boolean isShort, StringBuffer buffer, String businessCode) { + String date = DateConvertUtils.getNow("yyyyMMdd"); + if (isShort) { + date = DateConvertUtils.getNow("yyyyMMdd"); + } + + buffer.append(date).append(IP_AFTER_TWO).append(formatNumber((long)getSeq(), 8)).append(businessCode).append(this.customOrderKey()); + if (buffer.length() > 29) { + throw new SysException("", "orderNo to long, orderNo->" + buffer.toString()); + } else { + return buffer.toString(); + } + } + + public abstract String customOrderKey(); + + public abstract String customKey(); + + private static int getSeq() { + int result = SEQ_ATOMIC_INTEGER.incrementAndGet(); + if (result <= SEQ_MAX) { + return result; + } else { + SEQ_ATOMIC_INTEGER.set(SEQ_MIN); + return SEQ_MIN; + } + } + + private static String formatNumber(long number, int digits) { + NumberFormat nf = NumberFormat.getInstance(); + nf.setMaximumIntegerDigits(digits); + nf.setMinimumIntegerDigits(digits); + nf.setGroupingUsed(false); + return nf.format(number); + } + + static { + String ip; + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + ip = inetAddress.getHostAddress(); + ip = ip.substring(ip.lastIndexOf(46) + 1); + ip = formatNumber(Long.valueOf(ip), IP_DIGITS); + } catch (Exception var4) { + LOGGER.error("can not get ip address", var4); + Random random = new Random((long)UUID.randomUUID().toString().hashCode()); + int randomNum = random.nextInt(IP_MOD); + ip = formatNumber((long)randomNum, IP_DIGITS); + } + + IP_AFTER_TWO = ip; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/AppleWebhookVerifier.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/AppleWebhookVerifier.java new file mode 100644 index 0000000..7724d12 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/AppleWebhookVerifier.java @@ -0,0 +1,149 @@ +package com.sonic.lion.utils; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.*; +import java.security.interfaces.ECPublicKey; +import java.util.*; + +/** + * Apple StoreKit V2 Webhook 签名验证工具类 + */ +@Component +@Slf4j +public class AppleWebhookVerifier { + + /** + * 证书下载地址:https://www.apple.com/certificateauthority/ + */ + private static final String ROOT_CA_PATH = "certificates/AppleRootCA-G3.cer"; // Apple 根证书路径 + private static final String WWDR_CA_PATH = "certificates/AppleWWDRCAG6.cer"; // Apple WWDR 中间证书路径 + + /** + * 处理 Apple Webhook 通知并验证签名 + * @param jwsToken JWS 格式的 webhook 通知 + * @return 验证结果,成功返回 "Webhook processed successfully",失败返回错误信息 + */ + public boolean handleAppleWebhook(String jwsToken) { + try { + // 解析 JWS + DecodedJWT jwt = JWT.decode(jwsToken); + + // 获取 x5c 证书链 + String[] x5c = jwt.getHeaderClaim("x5c").asArray(String.class); + if (x5c == null || x5c.length < 2) { + log.info("===> handleAppleWebhook : 错误:无效的 x5c 证书链,长度不足"); + return false; + } + + // 解析证书链并打印调试信息 + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + List certChain = new ArrayList<>(); + for (int i = 0; i < x5c.length; i++) { + byte[] certBytes = Base64.getDecoder().decode(x5c[i]); + X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certBytes)); + certChain.add(cert); + // 打印证书信息 + log.info("证书 " + i + " Subject: " + cert.getSubjectX500Principal()); + log.info("证书 " + i + " Issuer: " + cert.getIssuerX500Principal()); + log.info("证书 " + i + " Serial: " + cert.getSerialNumber()); + log.info("证书 " + i + " 有效期: " + cert.getNotBefore() + " 至 " + cert.getNotAfter()); + } + + // 加载 Apple 根证书和 WWDR G6 中间证书 + X509Certificate rootCert = loadCertificateV2(ROOT_CA_PATH); + X509Certificate wwdrCert = loadCertificateV2(WWDR_CA_PATH); + log.info("根证书 Subject: " + rootCert.getSubjectX500Principal()); + log.info("根证书 有效期: " + rootCert.getNotBefore() + " 至 " + rootCert.getNotAfter()); + log.info("WWDR G6 证书 Subject: " + wwdrCert.getSubjectX500Principal()); + log.info("WWDR G6 证书 有效期: " + wwdrCert.getNotBefore() + " 至 " + wwdrCert.getNotAfter()); + + // 验证证书链 + try { + validateCertificateChain(certChain, rootCert, wwdrCert); + } catch (Exception e) { + log.info("===> handleAppleWebhook : 错误:证书链验证失败: {}", e.getMessage()); + return false; + } + + // 验证 JWS 签名 + X509Certificate signingCert = certChain.get(0); // 叶证书 + Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) signingCert.getPublicKey(), null); + JWT.require(algorithm).build().verify(jwsToken); + + //返回验证通过的结果 + return true; + } catch (JWTVerificationException e) { + log.error("===> handleAppleWebhook : 签名验证失败 :", e); + } catch (Exception e) { + log.error("===> handleAppleWebhook : 处理 webhook 失败 :", e); + } + return false; + } + + /** + * 加载证书文件 + * @param path 证书文件路径 + * @return X509Certificate 证书对象 + * @throws Exception 证书加载失败 + */ + private X509Certificate loadCertificateV2(String path) throws CertificateException, IOException { + try (InputStream is = getClass().getClassLoader().getResourceAsStream(path)) { + if (is == null) { + throw new IOException("证书文件未找到: " + path); + } + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(is); + } + } + + /** + * 验证证书链 + * @param certChain x5c 中的证书链 + * @param rootCert Apple 根证书 + * @param wwdrCert Apple WWDR G6 中间证书 + * @throws Exception 证书链验证失败 + */ + private void validateCertificateChain(List certChain, X509Certificate rootCert, X509Certificate wwdrCert) + throws Exception { + // 创建信任锚 + TrustAnchor trustAnchor = new TrustAnchor(rootCert, null); + Set trustAnchors = new HashSet<>(); + trustAnchors.add(trustAnchor); + + // 构建验证链:使用 x5c 中的叶证书和中间证书 + List validationChain = new ArrayList<>(); + validationChain.add(certChain.get(0)); // 叶证书 + if (certChain.size() > 1) { + validationChain.add(certChain.get(1)); // WWDR CA G6 + } + + // 创建 CertPath + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + CertPath certPath = cf.generateCertPath(validationChain); + + // 配置 PKIX 参数 + PKIXParameters params = new PKIXParameters(trustAnchors); + params.setRevocationEnabled(false); // Apple 证书不使用 CRL/OCSP + params.setDate(null); // 使用当前时间验证 + + // 执行证书链验证 + try { + CertPathValidator validator = CertPathValidator.getInstance("PKIX"); + validator.validate(certPath, params); + log.info("证书链验证成功"); + } catch (CertPathValidatorException e) { + System.err.println("证书链验证失败: " + e.getMessage()); + throw e; + } + } + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/BeanConvert.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/BeanConvert.java new file mode 100644 index 0000000..cd57f0a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/BeanConvert.java @@ -0,0 +1,66 @@ +package com.sonic.lion.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cglib.beans.BeanCopier; + +import java.util.ArrayList; +import java.util.List; + +/** + * 对象拷贝 + * @author dev-center + */ +public class BeanConvert { + private final static Logger LOG = LoggerFactory.getLogger(BeanConvert.class); + + /** + * 实例类转换 + * + * @param source 源对象 + * @param target 目标对象 + * @param 源对象 + * @param 目标对象 + */ + public static K copeBean(T source, Class target) { + if (source == null) { + return null; + } + BeanCopier beanCopier = BeanCopier.create(source.getClass(), target, false); + K result = null; + try { + result = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(source, result, null); + return result; + } + + + + /** + * page实例类转换 + * + * @param 源对象 + * @param 目标对象 + * @param source 源对象 + * @param target 目标对象 + * @return + */ + public static List copeList(List source, Class target) { + List list = new ArrayList<>(); + for (T af : source) { + BeanCopier beanCopier = BeanCopier.create(af.getClass(), target, false); + K af1 = null; + try { + af1 = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(af, af1, null); + list.add(af1); + } + return list; + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/DateConvertUtils.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/DateConvertUtils.java new file mode 100644 index 0000000..1f835ea --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/DateConvertUtils.java @@ -0,0 +1,113 @@ +package com.sonic.lion.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.zone.ZoneRules; +import java.util.Calendar; +import java.util.Date; + +/** + * @Author code + * @Date 2025/8/11 17:30 + * @Version 1.0 + */ +@Slf4j +public class DateConvertUtils { + + public static String getNow(String pattern) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); + return dateTimeFormatter.format(LocalDateTime.now()); + } + + public static String format(LocalDateTime localDateTime, String pattern) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); + String date = dateTimeFormatter.format(localDateTime.minusHours(getLaHours())); + return date; + } + + public static String format(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = dateTimeFormatter.format(localDateTime.minusHours(getLaHours())); + return date; + } + + public static String format() { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("America/Los_Angeles")).toLocalDateTime()); + return date; + } + + public static Integer formatToInt(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + String date = dateTimeFormatter.format(localDateTime.minusHours(getLaHours())); + return Integer.valueOf(date); + } + + + public static Integer formatYearMonthToInt(LocalDateTime localDateTime) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM"); + String date = dateTimeFormatter.format(localDateTime.minusHours(getLaHours())); + return Integer.valueOf(date); + } + + public static Integer formatToInt(Date date) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); + return Integer.valueOf(simpleDateFormat.format(date)); + } + + public static Date getYesterdayDate(){ + Calendar cal=Calendar.getInstance(); + cal.add(Calendar.DATE,-1); + Date d=cal.getTime(); + return d; + } + + /** + * 获取洛杉矶 夏令时、冬令时 和北京时间的小时差 + * @return + */ + public static int getLaHours() { + boolean bl = false; + try { + bl = isDaylightTime(LocalDateTime.now(), ZoneId.of(ZoneId.SHORT_IDS.get("PST"))); + } catch (Exception e) { + log.info("处理异常情况 : {}", e); + } + int hours = bl ? 15 : 16; + return hours; + } + + /** + * 传入指定时间和时区 + * @param localDateTime + * @param zoneId + * @return true 表示是夏令时,false表示是冬令时 + */ + public static boolean isDaylightTime(LocalDateTime localDateTime, ZoneId zoneId) { + ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId); + ZoneRules rules = zoneId.getRules(); + boolean flag = rules.isDaylightSavings(zonedDateTime.toInstant()); + return flag; + } + + /** + * 根据时间戳获取时间对象 + * @param timestamp + * @return + */ + public static LocalDateTime getDate(Long timestamp) { + if(timestamp == null) { + return null; + } + Instant instant = Instant.ofEpochSecond(timestamp); + LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + return localDateTime; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/HttpUtil.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/HttpUtil.java new file mode 100644 index 0000000..1cfe0f6 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/HttpUtil.java @@ -0,0 +1,74 @@ +package com.sonic.lion.utils; + +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +/** + * Http + **/ +public class HttpUtil { + + /** + * 获取body内容 + * + * @param request + * @return + * @throws Exception + */ + public static String getBody(HttpServletRequest request) throws Exception { + String body; + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader bufferedReader = null; + try { + InputStream inputStream = request.getInputStream(); + if (inputStream != null) { + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + char[] charBuffer = new char[128]; + int bytesRead = -1; + while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { + stringBuilder.append(charBuffer, 0, bytesRead); + } + } else { + stringBuilder.append(""); + } + } catch (IOException ex) { + throw ex; + } finally { + if (bufferedReader != null) { + try { + bufferedReader.close(); + } catch (IOException ex) { + throw ex; + } + } + } + body = stringBuilder.toString(); + return body; + } + + + /** + * 获取请求头信息 + * + * @param request + * @return + */ + public static Map getHeadersInfo(HttpServletRequest request) { + Map map = new HashMap(); + @SuppressWarnings("rawtypes") + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String key = (String) headerNames.nextElement(); + String value = request.getHeader(key); + map.put(key, value); + } + return map; + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/IOSConsumptionUtil.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/IOSConsumptionUtil.java new file mode 100644 index 0000000..498afc9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/IOSConsumptionUtil.java @@ -0,0 +1,322 @@ +package com.sonic.lion.utils; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class IOSConsumptionUtil { + + @Value("${apple.boundId}") + private String BUNDLE_ID; + + @Autowired + private NetRetryUtils netRetryUtils; + + + /** + * + * "userStatus": 1, + * "consumptionStatus": 3, + * "lifetimeDollarsPurchased": 6, + * "appAccountToken": "", + * "sampleContentProvided": false, + * "playTime": 7, + * "lifetimeDollarsRefunded": 2, + * "customerConsented": true, + * "accountTenure": 3, + * "deliveryStatus": 0, + * "platform": 1 + * @param originalTransactionId + * @param age + * @param consumptionStatus + * @param totalBuy + * @param totalRefund + * @param playTimeDays + */ + public String sendConsumption(String originalTransactionId , Integer age, int consumptionStatus, int totalBuy,int totalRefund ,int playTimeDays) { + JSONObject request = new JSONObject(); + int ageStatus = 0; + if (age > 0 && age < 3) { + ageStatus = 1; + } else if (age >= 3 && age < 10) { + ageStatus = 2; + } else if (age >= 10 && age < 30) { + ageStatus = 3; + } else if (age >= 30 && age < 90) { + ageStatus = 4; + } else if (age >= 90 && age < 180) { + ageStatus = 5; + } else if (age >= 180 && age < 365) { + ageStatus = 6; + } else if (age >= 365) { + ageStatus = 7; + } + /** + * 0 + * Account age is undeclared. + * + * 1 + * Account age is between 0–3 days. + * + * 2 + * Account age is between 3–10 days. + * + * 3 + * Account age is between 10–30 days. + * + * 4 + * Account age is between 30–90 days. + * + * 5 + * Account age is between 90–180 days. + * + * 6 + * Account age is between 180–365 days. + * + * 7 + * Account age is over 365 days. + */ + request.put("accountTenure", ageStatus); + request.put("appAccountToken", ""); + + /** + * 0 + * 消费状态未申报。 + * + * 1 + * 不消耗应用内购买。 + * + * 2 + * 应用内购买被部分消费。 + * + * 3 + * 应用内购买已完全消耗。 + */ + request.put("consumptionStatus", consumptionStatus); + /** + * 将此字段设置为true如果用户同意向 App Store 发送与其退款请求相关的消费数据,包括. 如果没有,请不要回复通知。ConsumptionRequestCONSUMPTION_REQUEST + */ + request.put("customerConsented", true); + /** + * 0 + * 该应用程序交付了消耗品的应用程序内购买,并且运行正常。 + * + * 1 + * 由于质量问题,该应用程序未提供应用程序内购买的消耗品。 + * + * 2 + * 该应用程序交付了错误的项目。 + * + * 3 + * 由于服务器中断,该应用程序没有提供应用程序内购买的消耗品。 + * + * 4 + * 由于游戏内货币的变化,该应用程序没有提供消耗品的应用程序内购买。 + * + * 5 + * 由于其他原因,该应用程序没有提供应用程序内购买的消耗品。 + */ + request.put("deliveryStatus", 0); + + /** + * 一个值,表示自购买该应用以来,客户在您的应用中在所有平台上进行的应用内购买金额。 + * 可用性 + * + * 应用商店服务器 API 1.0+ + * 框架 + * + * 应用商店服务器 API + * 在此页 + * + * 宣言 + * 可能值 + * 也可以看看 + * 宣言 + * number lifetimeDollarsPurchased + * 可能的值 + * 0 + * 终身购买金额未申报。 + * + * 1 + * 终身购买金额为 0 美元。 + * + * 2 + * 终身购买金额在 0.01-49.99 美元之间。 + * + * 3 + * 终身购买金额在 50-99.99 美元之间。 + * + * 4 + * 终身购买金额在 100-499.99 美元之间。 + * + * 5 + * 终身购买金额在 500-999.99 美元之间。 + * + * 6 + * 终身购买金额在 1000-1999.99 美元之间。 + * + * 7 + * 终身购买金额超过 2000 美元。 + */ + int lifetimeDollarsPurchased = 0; + if(totalBuy==0){ + lifetimeDollarsPurchased = 1; + }else if(totalBuy>0 && totalBuy< 5000 ){ + lifetimeDollarsPurchased = 2; + }else if(totalBuy>=5000 && totalBuy< 10000 ){ + lifetimeDollarsPurchased = 3; + }else if(totalBuy>=10000 && totalBuy< 50000 ){ + lifetimeDollarsPurchased = 4; + }else if(totalBuy>=50000 && totalBuy< 100000 ){ + lifetimeDollarsPurchased = 5; + }else if(totalBuy>=100000 && totalBuy< 200000 ){ + lifetimeDollarsPurchased = 6; + }else if(totalBuy>=200000){ + lifetimeDollarsPurchased = 7; + } + request.put("lifetimeDollarsPurchased", lifetimeDollarsPurchased); + + + int lifetimeDollarsRefunded = 0 ; + if(totalRefund==0){ + lifetimeDollarsPurchased = 1; + }else if(totalBuy>0 && totalBuy< 5000 ){ + lifetimeDollarsRefunded = 2; + }else if(totalBuy>=5000 && totalBuy< 10000 ){ + lifetimeDollarsRefunded = 3; + }else if(totalBuy>=10000 && totalBuy< 50000 ){ + lifetimeDollarsRefunded = 4; + }else if(totalBuy>=50000 && totalBuy< 100000 ){ + lifetimeDollarsRefunded = 5; + }else if(totalBuy>=100000 && totalBuy< 200000 ){ + lifetimeDollarsRefunded = 6; + }else if(totalBuy>=200000){ + lifetimeDollarsRefunded = 7; + } + /** + *0 + * 终身退款金额未申报。 + * + * 1 + * 终身退款金额为 0 美元。 + * + * 2 + * 终身退款金额在 0.01-49.99 美元之间。 + * + * 3 + * 终身退款金额在 50-99.99 美元之间。 + * + * 4 + * 终身退款金额在 100-499.99 美元之间。 + * + * 5 + * 终身退款金额在 500-999.99 美元之间。 + * + * 6 + * 终身退款金额在 1000-1999.99 美元之间。 + * + * 7 + * 终身退款金额超过 2000 美元。 + */ + request.put("lifetimeDollarsRefunded", lifetimeDollarsRefunded); + + /** + * 可能的值 + * 0 + * 未申报。 + * + * 1 + * 一个苹果平台。 + * + * 2 + * 非苹果平台。 + */ + request.put("platform", 1); + /** + * 指示客户使用应用程序的时间量的值。 + * 可用性 + * + * 应用商店服务器 API 1.0+ + * 框架 + * + * 应用商店服务器 API + * 在此页 + * + * 宣言 + * 可能值 + * 也可以看看 + * 宣言 + * number playTime + * 可能的值 + * 0 + * 订婚时间不详。 + * + * 1 + * 参与时间在 0-5 分钟之间。 + * + * 2 + * 参与时间在 5-60 分钟之间。 + * + * 3 + * 参与时间在1-6小时之间。 + * + * 4 + * 参与时间在 6-24 小时之间。 + * + * 5 + * 订婚时间在1-4天之间。 + * + * 6 + * 订婚时间在 4-16 天之间。 + * + * 7 + * 订婚时间超过16天。 + */ + request.put("playTime", 3); + if (playTimeDays > 1 && playTimeDays<= 4) { + request.put("playTime", 5); + } else if (playTimeDays> 4 && playTimeDays <= 16) { + request.put("playTime", 6); + } else if (playTimeDays> 16) { + request.put("playTime", 7); + } + /** + * 提供的示例内容 + * 一个布尔值,指示您在购买之前是否提供了内容的免费样本或试用版,或有关其功能的信息。 + */ + request.put("sampleContentProvided", false); + /** + * 0 + * 帐户状态未声明。 + * + * 1 + * 客户的帐户处于活动状态。 + * + * 2 + * 客户的账户被暂停。 + * + * 3 + * 客户的帐户被终止。 + * + * 4 + * 客户帐户的访问权限有限。 + */ + request.put("userStatus", 1); + log.info("request body:{}" ,request); + try{ + String token = JwtUtils.getAppleJwt(BUNDLE_ID); + String response = netRetryUtils.putForObject("https://api.storekit.itunes.apple.com/inApps/v1/transactions/consumption/" + originalTransactionId, request,token); + log.info("成功提交IOS 消费信息 response:{}" ,response); + }catch (Exception e){ + log.info("成功提交IOS 消费信息 失败"); + e.printStackTrace(); + } + return JSON.toJSONString(request); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/JwtUtils.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/JwtUtils.java new file mode 100644 index 0000000..64a187c --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/JwtUtils.java @@ -0,0 +1,68 @@ +package com.sonic.lion.utils; + +import com.google.common.collect.ImmutableMap; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Date; +import java.util.UUID; + + +@Slf4j +public class JwtUtils { + + public static final String KID = "8ZWMTXTQMS"; + public static final String ISSUER_ID = "567ad6cc-22b8-44aa-8ff0-7f2b88f1c418"; + private static final String PK = "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgv+SthbyYfUmy/+r0OlUl0vDynQWf+AH3zazlm/zeM3agCgYIKoZIzj0DAQehRANCAAQ/NG6BS0y6zI+ITjJGoLPW4Ucdto8B/9lAnzkTVNCrJ5MNFbp6nS0+AlfnbwgELGOIR0Zz0YbGAINZ9qxcGyGz"; + + public static String getAppleJwt(String bundleId) throws Exception { + long nowMillis = System.currentTimeMillis();//生成JWT的时间 + long expMillis = nowMillis + 200000; + Date now = new Date(nowMillis); + Date exp = new Date(expMillis); + + JwtBuilder builder = Jwts.builder().setHeaderParam("kid", KID) + .setClaims(ImmutableMap.of("bid", bundleId, "nonce", UUID.randomUUID().toString())) + .setIssuer(ISSUER_ID) + .setIssuedAt(now) + .setAudience("appstoreconnect-v1") + .setExpiration(exp) + .signWith(getPk()); + return builder.compact(); + } + + public static String getJwtBody(String jwtToken) { + if(StringUtils.isEmpty(jwtToken)) { + return null; + } + System.out.println("------------ Decode JWT ------------"); + String[] split_string = jwtToken.split("\\."); + String base64EncodedHeader = split_string[0]; + String base64EncodedBody = split_string[1]; + String base64EncodedSignature = split_string[2]; + + System.out.println("~~~~~~~~~ JWT Body ~~~~~~~"); + String body = new String(Base64.getDecoder().decode(base64EncodedBody)); + return body; + } + + /** + * 根据 私钥字符串获取 PrivateKey 对象 + * + * @return + * @throws Exception + */ + public static PrivateKey getPk() throws Exception { + byte[] encoded = Base64.getDecoder().decode(PK); + PKCS8EncodedKeySpec kspec = new PKCS8EncodedKeySpec(encoded); + KeyFactory kf = KeyFactory.getInstance("EC"); //ES256 算法。 + PrivateKey unencryptedPrivateKey = kf.generatePrivate(kspec); + return unencryptedPrivateKey; + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/KeyGenerator.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/KeyGenerator.java new file mode 100644 index 0000000..7fddfe7 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/KeyGenerator.java @@ -0,0 +1,40 @@ +package com.sonic.lion.utils; + +import com.sonic.lion.enums.PayGenrtatorCodeType; + +import java.util.UUID; + +public class KeyGenerator extends AbstractKeyGenerator { + private static final KeyGenerator SINGLE = new KeyGenerator(); + + private KeyGenerator() { + } + + @Override + public String customOrderKey() { + return ""; + } + + public static KeyGenerator instance() { + return SINGLE; + } + + public static String UUID() { + return UUID.randomUUID().toString(); + } + + @Override + public String customKey() { + return ""; + } + + /** + * 编号生成规则 YYYYMMDD+2位业务代码+9位流水号 + * @param bizType 业务类型 + * @author Xi.He + * @return 流水号 + */ + public String generatorUniqueKey(PayGenrtatorCodeType bizType) { + return super.generatorUniqueKey(String.valueOf(bizType.getValue())); + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/LimitUtils.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/LimitUtils.java new file mode 100644 index 0000000..0c3353a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/LimitUtils.java @@ -0,0 +1,84 @@ +package com.sonic.lion.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 限流工具类 + */ +@Slf4j +@Component +public class LimitUtils { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * redis key不存在的过期时间值 + */ + private Integer NULL_KEY_EXP_TIME_VALUE = -2; + + /** + * redis key永不过期的过期时间值 + */ + private Integer NEVER_EXPIRES_VALUE = -1; + + /** + * 根据请求参数进行限流检查 + * @param redisKey 自定义封装的redisKey + * @param count 限流的数量 + * @param time 时间段:单位为秒 + */ + public boolean defaultLimitCheckByKey(String redisKey, int count, int time) { + //用户操作某个话题的点赞,超过4次就提示,锁定5s + int num; + Long expTime = stringRedisTemplate.getExpire(redisKey); + if (expTime == null || expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", time, TimeUnit.SECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(redisKey, 1).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis并返回false + stringRedisTemplate.delete(redisKey); + return false; + } + if (num > count) { + log.info("===>超过了限定的次数[" + count + "]"); + return true; + } + return false; + } + + /** + * 根据请求参数进行限流检查 + * @param redisKey 自定义封装的redisKey + * @param time 时间段:单位为秒 + */ + public int defaultLimitCheckReturnCount(String redisKey, int time) { + //用户操作某个话题的点赞,超过4次就提示,锁定5s + int num; + Long expTime = stringRedisTemplate.getExpire(redisKey); + if (expTime == null || expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", time, TimeUnit.SECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(redisKey, 1).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis并返回false + stringRedisTemplate.delete(redisKey); + return 0; + } + return num; + } + + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/MD5Utils.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/MD5Utils.java new file mode 100644 index 0000000..a48ccf9 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/MD5Utils.java @@ -0,0 +1,25 @@ +package com.sonic.lion.utils; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class MD5Utils { + + public static String stringToMD5(String plainText) { + byte[] secretBytes = null; + try { + secretBytes = MessageDigest.getInstance("md5").digest( + plainText.getBytes()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("没有这个md5算法!"); + } + String md5code = new BigInteger(1, secretBytes).toString(16); + final int length = md5code.length(); + for (int i = 0; i < 32 - length; i++) { + md5code = "0" + md5code; + } + return md5code; + } + +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/MaskUtils.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/MaskUtils.java new file mode 100644 index 0000000..f82ea8a --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/MaskUtils.java @@ -0,0 +1,53 @@ +package com.sonic.lion.utils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +@Slf4j +public class MaskUtils { + + /** + * 隐藏邮箱信息 + * + * @param email + * @return + */ + public static String maskEmail(String email) { + if (StringUtils.isEmpty(email)) return ""; + + String prefix = ""; + try { + if (email.indexOf("@") > 5) { + prefix = email.substring(0, 3); + } else { + prefix = email.substring(0, 1); + } + + String middle = ""; + if (prefix.length() < email.indexOf("@")) { + for (int i = prefix.length(); i < email.indexOf("@"); i++) { + middle += "*"; + } + } + + String suffix = email.substring(email.indexOf("@")); + return prefix + middle + suffix; + } catch (Exception e) { + log.error( + String.format("email encrypt fail, param:%s, errorMsg:%s", email, e.getMessage()), e + ); + return ""; + } + } + + /** + * 隐藏身份证号码 + * + * @param id + * @return + */ + public static String maskBankCardNo(String id) { + return "****" + id.substring(id.length() - 4); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/MoneyUtils.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/MoneyUtils.java new file mode 100644 index 0000000..795b1e5 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/MoneyUtils.java @@ -0,0 +1,36 @@ +package com.sonic.lion.utils; + +import java.math.BigDecimal; + +public class MoneyUtils { + + + /** + * 单位转换,分转换成元 + * + * @param cent (分) + * @return BigDecimal (元) + */ + public static BigDecimal centToDollar(Long cent) { + if (null == cent) + return null; + + return new BigDecimal(cent).divide(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP); + } + + /** + * 计算手续费,四舍五入0位小数 + * + * @param money + * @param rate + * @return + */ + public static Long calculateFee(Long money, Double rate) { + if (money == null || rate == null) + return null; + + return new BigDecimal(money) + .multiply(new BigDecimal(rate)).setScale(0, BigDecimal.ROUND_HALF_UP).longValue(); + + } +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/NetRetryUtils.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/NetRetryUtils.java new file mode 100644 index 0000000..1dbe1bb --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/NetRetryUtils.java @@ -0,0 +1,50 @@ +package com.sonic.lion.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Component +public class NetRetryUtils { + + @Autowired + private RestTemplate restTemplate; + + @Retryable(value = RestClientException.class, maxAttempts = 5, + backoff = @Backoff(delay = 5000L, multiplier = 1.5)) + public T postForObject(String url, @Nullable Object request, Class responseType, Object... uriVariables) { + return restTemplate.postForObject(url, request, responseType, uriVariables); + } + + @Retryable(value = RestClientException.class, maxAttempts = 5, + backoff = @Backoff(delay = 5000L, multiplier = 1.5)) + public String getForObject(String url, String authorization) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + authorization); + HttpEntity requestEntity = new HttpEntity( headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); + return response.getBody(); + } + + @Retryable(value = RestClientException.class, maxAttempts = 5, + backoff = @Backoff(delay = 5000L, multiplier = 1.5)) + public String putForObject(String url, @Nullable Object request, String authorization) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + authorization); + HttpEntity requestEntity = new HttpEntity(request, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, String.class); + log.info("===> putForObject response : {}", response); + return response.getBody(); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/NoDisplayBizNumSet.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/NoDisplayBizNumSet.java new file mode 100644 index 0000000..9244d41 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/NoDisplayBizNumSet.java @@ -0,0 +1,71 @@ +package com.sonic.lion.utils; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.google.common.collect.Sets; +import com.sonic.lion.domain.entity.AccountBuffBill; +import com.sonic.lion.domain.enums.InOrOut; +import com.sonic.lion.enums.BizType; +import com.sonic.lion.enums.I18nResources; + +import java.util.Set; + +public class NoDisplayBizNumSet { + + //不返回交易号 的这些业务类型 + public static final Set tradeNoSets = Sets.newHashSet(BizType.CHARGE, BizType.WITHDRAW + ); + + //返回 desc字段的 业务类型 + public static final Set simpleDescSets = Sets.newHashSet( + BizType.CHARGE); + + + //处理Income Outcome的翻译。 + public static final Set inOrOutBizTypeSets = Sets.newHashSet(); + + + public static String getItemDesc(AccountBuffBill buffBill, boolean mobile) { + BizType bizType = buffBill.getBizType(); + InOrOut inOrOut = buffBill.getInOrOut(); + + //TODO 先暂时写死描述,有待完善 + if (bizType != null) { + return bizType.getDesc(); + } + + // 先处理移动端口 和 web 异同之处 + if (BizType.WITHDRAW.equals(bizType) && mobile) { + if (inOrOut == InOrOut.IN) { + return I18nResources.BILL_OTHER_INCOME.getI18n(); + } else if (inOrOut == InOrOut.OUT) { + return I18nResources.BILL_OTHER_OUTCOME.getI18n(); + } + } + if (BizType.WITHDRAW.equals(bizType) && !mobile) { + if (buffBill.getWithdrawStatus() != null && AccountBuffBill.WithdrawStatus.WITHDRAW_FAIL_BACK.equals(buffBill.getWithdrawStatus())) { + return I18nResources.BILL_WITHDRAW_FAIL_BACK.getI18n(); + } + if (inOrOut == InOrOut.IN) { + return I18nResources.BILL_WITHDRAW_FAIL.getI18n(); + } else if (inOrOut == InOrOut.OUT) { + return I18nResources.BILL_BUFF_WITHDRAW.getI18n(); + } + } + + //不需要处理 英文就是desc + if (simpleDescSets.contains(bizType)) { + return bizType.getDesc(); + } + + //处理Income Outcome的翻译。 + if (inOrOutBizTypeSets.contains(bizType)) { + return I18nResources.valueOf(bizType.name() + "_" + inOrOut.getDescInEnglish().toUpperCase()).getI18n(); + } + + if (bizType.equals(BizType.REFUND)) { + return StringUtils.isNotEmpty(buffBill.getBizNoRelationNo()) ? I18nResources.BILL_ORDER_REWARD_REFUND.getI18n() : I18nResources.BILL_GAME_REFUND.getI18n(); + + } + return ""; + } +} \ No newline at end of file diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/RedisKeyUtils.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/RedisKeyUtils.java new file mode 100644 index 0000000..5e95f39 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/RedisKeyUtils.java @@ -0,0 +1,49 @@ +package com.sonic.lion.utils; + +import com.sonic.common.AppRuntime; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 统一的 redis key 管理工具类(方便后续的维护和查找) + * + * @Author code + * @Date 2021/9/24 + * @Version 1.0 + */ +@Slf4j +@Service +public class RedisKeyUtils { + + @Autowired + private AppRuntime appRuntime; + + /** + * 下单总数限制 + * + * @return + */ + public String createPrePayTrade(Long userId) { + return appRuntime.buildPrefixKey("lock", "createPrePayTradeV1", userId); + } + + /** + * 提现锁 + * @param userId + * @return + */ + public String withdrawLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "withdraw", userId); + } + + /** + * 订阅会话缓存 + * @param userId + * @return + */ + public String subSessionIdCacheKey(Long userId) { + return appRuntime.buildPrefixKey("sub", "sessionId", userId); + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/TransactionUtils.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/TransactionUtils.java new file mode 100644 index 0000000..dcf15c1 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/TransactionUtils.java @@ -0,0 +1,49 @@ +package com.sonic.lion.utils; + +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.util.concurrent.Executor; + +/** + * 事务工具类 + * @author huzhihui + * @version $ v 0.1 2022/3/25 9:36 Exp $$ + */ +public class TransactionUtils { + + /** + * 在事务提交后同步执行 + * @param runnable + */ + public static void afterCommitSyncExecute(Runnable runnable){ + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public void afterCommit() { + runnable.run(); + } + }); + } else { + runnable.run(); + } + } + + /** + * 在事务提交后异步执行 + * @param runnable + */ + public static void afterCommitAsyncExecute(Executor executor, Runnable runnable){ + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public void afterCommit() { + executor.execute(runnable); + } + }); + } else { + executor.execute(runnable); + } + } + +} diff --git a/sonic-lion/server/src/main/java/com/sonic/lion/utils/UUIDGenerator.java b/sonic-lion/server/src/main/java/com/sonic/lion/utils/UUIDGenerator.java new file mode 100644 index 0000000..0a63b63 --- /dev/null +++ b/sonic-lion/server/src/main/java/com/sonic/lion/utils/UUIDGenerator.java @@ -0,0 +1,104 @@ +package com.sonic.lion.utils; + +import java.util.Random; +import java.util.UUID; + +/** + * @author code + */ +public class UUIDGenerator { + + private static final String SPLITOR = "-"; + private static final String BLANK = ""; + + public static String BASE_STRING = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + public static String[] BASE_CHARS = new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", + "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", + "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", + "T", "U", "V", "W", "X", "Y", "Z"}; + + /** UUID 32位 */ + public static String generateLongUuid() { + return generateLongUuid(true); + } + + /** UUID 32位 */ + public static String generateLongUuid(boolean isUpperCase) { + String uuid = UUID.randomUUID().toString().replace(SPLITOR, BLANK); + if (isUpperCase) { + return uuid.toUpperCase(); + } + return uuid.toLowerCase(); + } + + /** UUID转为8位 */ + public static String generateShortUuid() { + StringBuilder shortBuffer = new StringBuilder(8); + String uuid = UUID.randomUUID().toString().replace(SPLITOR, BLANK); + for (int i = 0; i < 8; i++) { + String str = uuid.substring(i * 4, i * 4 + 4); + int x = Integer.parseInt(str, 16); + shortBuffer.append(BASE_CHARS[x % 0x3E]); + } + return shortBuffer.toString(); + } + + public static String generate16Uuid() { + StringBuilder shortBuffer = new StringBuilder(16); + String uuid = UUID.randomUUID().toString().replace(SPLITOR, BLANK); + for (int i = 0; i < 16; i++) { + int start = i * 2; + int end = Math.min(i * 2 + 4, 32); + String str = uuid.substring(start, end); + int x = Integer.parseInt(str, 16); + shortBuffer.append(BASE_CHARS[x % 0x3E]); + } + return shortBuffer.toString(); + } + + /** + * 生成32位的token + * @return + */ + public static String generate32Uuid() { + return generate16Uuid() + generate16Uuid(); + } + + /** + * 生成48位的token + * @return + */ + public static String generate48Uuid() { + return generate16Uuid() + generate16Uuid() + generate16Uuid(); + } + + /** + * 生成64位的token + * @return + */ + public static String generate64Uuid() { + return generate16Uuid() + generate16Uuid() + generate16Uuid() + generate16Uuid(); + } + + /** length表示生成字符串的长度 */ + public static String getRandomString(int length) { + Random random = new Random(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + int number = random.nextInt(BASE_STRING.length()); + sb.append(BASE_STRING.charAt(number)); + } + return sb.toString(); + } + + public static void main(String[] args) { + long start = System.currentTimeMillis(); + for (int i = 0; i < 30000; i++) { + System.out.println("D" + generateShortUuid() + getRandomString(0)); + } + System.out.println(System.currentTimeMillis() - start); + } + +} + diff --git a/sonic-lion/server/src/main/resources/application-dev.yml b/sonic-lion/server/src/main/resources/application-dev.yml new file mode 100644 index 0000000..788227d --- /dev/null +++ b/sonic-lion/server/src/main/resources/application-dev.yml @@ -0,0 +1,88 @@ +spring: + datasource: + url: jdbc:mysql://54.223.196.180:3306/sonic-lion?useSSL=false&characterEncoding=utf-8&autoReconnect=true + username: root + password: toukagames1234 + + redis: + database: 0 + host: 54.223.196.180 + port: 6379 + password: 123456 + # cluster: + # nodes: 192.168.100.238:6379 + # ssl: + # enabled: true + + # TODO 这个谁都不允许修改,这个是qa环境专用的,要在本地环境启动的话就启动前面的配置吧 + # rabbitmq: + # host: 192.168.100.238 + # port: 5672 + # username: guest + # password: epal@2020 + # listener: + # simple: + # acknowledge-mode: manual + # #初始连接数量 + # concurrency: 5 + # #最大连接数量 + # max-concurrency: 10 + # #限流 + # prefetch: 1 + rabbitmq: + host: 54.223.196.180 + port: 5672 + username: guest + password: toukagames1234 + listener: + simple: + acknowledge-mode: manual + #初始连接数量 + concurrency: 5 + #最大连接数量 + max-concurrency: 10 + #限流 + prefetch: 1 + +apple: + boundId: com.epal.epal + environment: Sandbox + password: d66b6c9531394883a3732526c46d4aca + verifyReceiptUrl: https://sandbox.itunes.apple.com/verifyReceipt + paymentPlatformFeeRate: 0 + +google: + boundId: com.epal.epal + serviceAccountId: in-app-android@api-5896133466559663343-485116.iam.gserviceaccount.com + packageName: com.epal.android + paymentPlatformFeeRate: 0 + projectId: e-pal-145ca + subscriptionId: es-vipsub-sub + +stripe: + apiKey: sk_test_51RoLASRHHBJ3NEeaS8VxcRsjapRASYt4lM4HHn22ZKbHMBzwce0kgJPECtoRcntzG4fktfjoDvHE4Pmx8Vpy8bLU008zUxKdxB + webhookSecret: xxx + mode: sandbox + paymentFeeBase: 30 + paymentFeeRate: 0.129 + withdrawFeeBase: 25 + withdrawFeeRate: 0.2 + webhookSec: + payment: xx + payout: xx + subscription: xx + dispute: xx + session: + successUrl: https://test.crushlevel.ai + cancelUrl: https://test.crushlevel.ai + sub: + successUrl: https://test.crushlevel.ai + cancelUrl: https://test.crushlevel.ai + portal: + returnUrl: xxx + +swagger: + # 关闭当前服务的 swagger ps:大家别把swagger暴露到公网上有风险,只在dev环境中打开 + enabled: false + base: + package: com.xxx.xxx diff --git a/sonic-lion/server/src/main/resources/application-local.yml b/sonic-lion/server/src/main/resources/application-local.yml new file mode 100644 index 0000000..a65d219 --- /dev/null +++ b/sonic-lion/server/src/main/resources/application-local.yml @@ -0,0 +1,74 @@ +spring: + datasource: + url: jdbc:mysql://34.220.65.35/sonic-lion?useSSL=false&characterEncoding=utf-8&autoReconnect=true + username: epal_devel + password: nOSRhnU5ETTQvfp5Q6jmjuSwzFQD3R + + redis: + database: 0 + host: 192.168.100.238 + port: 6379 + password: Epal@2020 + cluster: + nodes: 192.168.100.238:6379 + ssl: + enabled: true + + # TODO 这个谁都不允许修改,这个是qa环境专用的,要在本地环境启动的话就启动前面的配置吧 + rabbitmq: + host: 192.168.100.238 + port: 5672 + username: guest + password: epal@2020 + listener: + simple: + acknowledge-mode: manual + #初始连接数量 + concurrency: 5 + #最大连接数量 + max-concurrency: 10 + #限流 + prefetch: 1 + +apple: + boundId: com.epal.epal + environment: Sandbox + password: d66b6c9531394883a3732526c46d4aca + verifyReceiptUrl: https://sandbox.itunes.apple.com/verifyReceipt + paymentPlatformFeeRate: 0 + +google: + boundId: com.epal.epal + serviceAccountId: in-app-android@api-5896133466559663343-485116.iam.gserviceaccount.com + packageName: com.epal.android + paymentPlatformFeeRate: 0 + projectId: e-pal-145ca + subscriptionId: es-vipsub-sub + +stripe: + apiKey: sk_test_51RoLASRHHBJ3NEeaS8VxcRsjapRASYt4lM4HHn22ZKbHMBzwce0kgJPECtoRcntzG4fktfjoDvHE4Pmx8Vpy8bLU008zUxKdxB + webhookSecret: xxx + mode: sandbox + paymentFeeBase: 30 + paymentFeeRate: 0.129 + withdrawFeeBase: 25 + withdrawFeeRate: 0.2 + webhookSec: + payment: xx + payout: xx + subscription: xx + dispute: xx + session: + successUrl: http://www.baidu.com + cancelUrl: http://www.google.com + sub: + successUrl: https://test.crushlevel.ai + cancelUrl: https://test.crushlevel.ai + portal: + returnUrl: xxx + +swagger: + # 关闭当前服务的 swagger ps:大家别把swagger暴露到公网上有风险,只在dev环境中打开 + enabled: false + base: + package: com.xxx.xxx diff --git a/sonic-lion/server/src/main/resources/application-product.yml b/sonic-lion/server/src/main/resources/application-product.yml new file mode 100644 index 0000000..d7b28ea --- /dev/null +++ b/sonic-lion/server/src/main/resources/application-product.yml @@ -0,0 +1,73 @@ +spring: + datasource: + url: ${DB.MASTER.LION.URL} + username: ${DB.MASTER.USERNAME} + password: ${DB.MASTER.PASSWORD} + + redis: + # TODO: 需要改写为测试环境的Redis地址, 数据库名称和密码 + host: ${REDIS.MAIN.HOST} + port: ${REDIS.MAIN.PORT} + database: 0 + password: ${REDIS.MAIN.PASSWORD} + + rabbitmq: + # TODO: 需要改写为测试环境的rabbitmq地址, 用户名和密码 + host: ${MQ.HOST} + port: ${MQ.PORT} + username: ${MQ.USERNAME} + password: ${MQ.PASSWORD} + ssl: + enabled: true + listener: + simple: + acknowledge-mode: manual + #初始连接数量 + concurrency: 5 + #最大连接数量 + max-concurrency: 10 + #限流 + prefetch: 1 + +apple: + boundId: ${APPLE.BOUND_ID} + environment: Sandbox + password: ${APPLE.PASSWORD} + verifyReceiptUrl: ${APPLE.VERIFY_RECEIPT_URL} + paymentPlatformFeeRate: 0 + +google: + boundId: ${GOOGLE.BOUND_ID} + serviceAccountId: ${GOOGLE.SERVICE_ACCOUNT_ID} + packageName: ${GOOGLE.PACKAGE_NAME} + paymentPlatformFeeRate: 0 + projectId: ${GOOGLE.PROJECT_ID} + subscriptionId: ${GOOGLE.SUBSCRIPTION_ID} + +stripe: + apiKey: ${STRIPE.API_KEY} + webhookSecret: ${STRIPE.WEBHOOK_SECRET} + mode: ${STRIPE.MODE} + paymentFeeBase: ${STRIPE.PAYMENT_FEE_BASE} + paymentFeeRate: ${STRIPE.PAYMENT_FEE_RATE} + withdrawFeeBase: ${STRIPE.WITHDRAW_FEE_BASE} + withdrawFeeRate: ${STRIPE.WITHDRAW_FEE_RATE} + webhookSec: + payment: ${STRIPE.WEBHOOK_SECRET.PAYMENT} + payout: ${STRIPE.WEBHOOK_SECRET.PAYOUT} + subscription: ${STRIPE.WEBHOOK_SECRET.SUBSCRIPTION} + dispute: ${STRIPE.WEBHOOK_SECRET.DISPUTE} + session: + successUrl: ${STRIPE.SESSION.SUCCESS_URL} + cancelUrl: ${STRIPE.SESSION.CANCEL_URL} + sub: + successUrl: ${STRIPE.SUB.SUCCESS_URL} + cancelUrl: ${STRIPE.SUB.CANCEL_URL} + portal: + returnUrl: ${STRIPE.PORTAL.RETURN_URL} + +swagger: + # 关闭当前服务的 swagger ps:大家别把swagger暴露到公网上有风险,只在dev环境中打开 + enabled: false + base: + package: com.xxx.xxx diff --git a/sonic-lion/server/src/main/resources/application-test.yml b/sonic-lion/server/src/main/resources/application-test.yml new file mode 100644 index 0000000..ca47816 --- /dev/null +++ b/sonic-lion/server/src/main/resources/application-test.yml @@ -0,0 +1,81 @@ +spring: + datasource: + url: ${DB.MASTER.LION.URL} + username: ${DB.MASTER.USERNAME} + password: ${DB.MASTER.PASSWORD} + + redis: + # TODO: 需要改写为测试环境的Redis地址, 数据库名称和密码 +# host: ${REDIS.MAIN.HOST} +# port: ${REDIS.MAIN.PORT} +# database: 0 +# password: ${REDIS.MAIN.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + ssl: + enabled: true + + rabbitmq: + # TODO: 需要改写为测试环境的rabbitmq地址, 用户名和密码 + host: ${MQ.HOST} + port: ${MQ.PORT} + username: ${MQ.USERNAME} + password: ${MQ.PASSWORD} + ssl: + enabled: true + listener: + simple: + acknowledge-mode: manual + #初始连接数量 + concurrency: 5 + #最大连接数量 + max-concurrency: 10 + #限流 + prefetch: 1 + +apple: + boundId: ${APPLE.BOUND_ID} + environment: Sandbox + password: ${APPLE.PASSWORD} + verifyReceiptUrl: ${APPLE.VERIFY_RECEIPT_URL} + paymentPlatformFeeRate: 0 + +google: + boundId: ${GOOGLE.BOUND_ID} + serviceAccountId: ${GOOGLE.SERVICE_ACCOUNT_ID} + packageName: ${GOOGLE.PACKAGE_NAME} + paymentPlatformFeeRate: 0 + projectId: ${GOOGLE.PROJECT_ID} + subscriptionId: ${GOOGLE.SUBSCRIPTION_ID} + +stripe: + apiKey: ${STRIPE.API_KEY} + webhookSecret: ${STRIPE.WEBHOOK_SECRET} + mode: ${STRIPE.MODE} + paymentFeeBase: ${STRIPE.PAYMENT_FEE_BASE} + paymentFeeRate: ${STRIPE.PAYMENT_FEE_RATE} + withdrawFeeBase: ${STRIPE.WITHDRAW_FEE_BASE} + withdrawFeeRate: ${STRIPE.WITHDRAW_FEE_RATE} + webhookSec: + payment: ${STRIPE.WEBHOOK_SECRET.PAYMENT} + payout: ${STRIPE.WEBHOOK_SECRET.PAYOUT} + subscription: ${STRIPE.WEBHOOK_SECRET.SUBSCRIPTION} + dispute: ${STRIPE.WEBHOOK_SECRET.DISPUTE} + session: +# successUrl: ${STRIPE.SESSION.SUCCESS_URL} +# cancelUrl: ${STRIPE.SESSION.CANCEL_URL} + successUrl: https://test.crushlevel.ai + cancelUrl: https://test.crushlevel.ai + sub: +# successUrl: ${STRIPE.SUB.SUCCESS_URL} +# cancelUrl: ${STRIPE.SUB.CANCEL_URL} + successUrl: https://test.crushlevel.ai + cancelUrl: https://test.crushlevel.ai + portal: + returnUrl: ${STRIPE.PORTAL.RETURN_URL} + +swagger: + # 关闭当前服务的 swagger ps:大家别把swagger暴露到公网上有风险,只在dev环境中打开 + enabled: false + base: + package: com.xxx.xxx diff --git a/sonic-lion/server/src/main/resources/application.yml b/sonic-lion/server/src/main/resources/application.yml new file mode 100644 index 0000000..0379bea --- /dev/null +++ b/sonic-lion/server/src/main/resources/application.yml @@ -0,0 +1,70 @@ +spring: + profiles: + # profile目前支持以下5种:local/unittest/dev/test/prod + # 开发的时候一般使用dev或者local + # 在测试环境/生产环境,该配置不起作用,会被外部传入的jvm启动参数(spring.profiles.active)或者环境变量覆盖 + active: dev + application: + name: lion + # 必须使用引号,否则会转成8进制 + id: "1004" + task: + execution: + pool: + max-size: 50 + core-size: 4 + queue-capacity: 20480 + keep-alive: 30s + datasource: + driver-class-name: com.mysql.jdbc.Driver + hikari: + auto-commit: true + connection-timeout: 20000 + maximum-pool-size: 30 + minimum-idle: 5 + idle-timeout: 300000 + max-lifetime: 1200000 + redis: + lettuce: + pool: + max-active: 1000 + max-wait: 1000 + max-idle: 100 + cache: + type: redis + +mybatis-plus: + # 定义mybatis映射文件的位置 + mapper-locations: classpath:/mapper/*Mapper.xml + type-enums-package: com.sonic.lion.domain;com.gamers.pay.pay.api.domain + +web: + frequency-alert: + # 以下配置含义为: /* 1秒访问20次, /index 10秒访问100次. 访问频率超过该规则会触发告警consumer + rules: + - /*:1/20 + - /index:10/100 + +mq: + exchange: + default-exchange: message-server-exchange + default: + queue: sonic-lion-queue + routing-key: sonic-lion-routing-key + +redisson: + nettyThreads: 8 +#内部API调用请求时需要进行的拦截操作(请求增加请求头,响应增加拦截判断) +sign: + #需要增加签名参数header头的前缀路径 多个用英文逗号分割 + addSignHeaderPrefixPath: /api/ + #需要校验header头签名参数的前缀路径 多个用英文逗号分割 + checkSignHeaderPrefixPath: /service/pay/trade/ + +# 禁用健康检查,有漏洞,任何人不的擅自打开【后果自负】 +management: + endpoints: + enabled-by-default: false #关闭监控 + +site: + type: main diff --git a/sonic-lion/server/src/main/resources/certificates/AppleRootCA-G3.cer b/sonic-lion/server/src/main/resources/certificates/AppleRootCA-G3.cer new file mode 100644 index 0000000..228bfa3 Binary files /dev/null and b/sonic-lion/server/src/main/resources/certificates/AppleRootCA-G3.cer differ diff --git a/sonic-lion/server/src/main/resources/certificates/AppleWWDRCAG6.cer b/sonic-lion/server/src/main/resources/certificates/AppleWWDRCAG6.cer new file mode 100644 index 0000000..424a70b Binary files /dev/null and b/sonic-lion/server/src/main/resources/certificates/AppleWWDRCAG6.cer differ diff --git a/sonic-lion/server/src/main/resources/json/abc.json b/sonic-lion/server/src/main/resources/json/abc.json new file mode 100644 index 0000000..335975f --- /dev/null +++ b/sonic-lion/server/src/main/resources/json/abc.json @@ -0,0 +1,50 @@ +//v2版本的订阅回调 +{ + "notificationType": "SUBSCRIBED", + "subtype": "INITIAL_BUY", + "notificationUUID": "e399ebdc-c0bf-49d3-8e80-252eb33c7280", + "data": { + "appAppleId": 6745584732, + "bundleId": "gg.epal.es", + "bundleVersion": "101", + "environment": "Sandbox", + "signedTransactionInfo": { + "transactionId": "2000000923404120", + "originalTransactionId": "2000000923404120", + "webOrderLineItemId": "2000000100070788", + "bundleId": "gg.epal.es", + "productId": "gg.epal.es.autonew.newvip1month", + "subscriptionGroupIdentifier": "21683358", + "purchaseDate": 1747810510000, + "originalPurchaseDate": 1747810511000, + "expiresDate": 1747810810000, + "quantity": 1, + "type": "Auto-Renewable Subscription", + "inAppOwnershipType": "PURCHASED", + "signedDate": 1747810527164, + "environment": "Sandbox", + "transactionReason": "PURCHASE", + "storefront": "USA", + "storefrontId": "143441", + "price": 6490, + "currency": "USD", + "appTransactionId": "704464682678600889" + }, + "signedRenewalInfo": { + "originalTransactionId": "2000000923404120", + "autoRenewProductId": "gg.epal.es.autonew.newvip1month", + "productId": "gg.epal.es.autonew.newvip1month", + "autoRenewStatus": 1, + "renewalPrice": 6490, + "currency": "USD", + "signedDate": 1747810527164, + "environment": "Sandbox", + "recentSubscriptionStartDate": 1747810510000, + "renewalDate": 1747810810000, + "appTransactionId": "704464682678600889" + }, + "status": 1 + }, + "version": "2.0", + "signedDate": 1747810527188 +} diff --git a/sonic-lion/server/src/main/resources/json/abc1.json b/sonic-lion/server/src/main/resources/json/abc1.json new file mode 100644 index 0000000..2cc708d --- /dev/null +++ b/sonic-lion/server/src/main/resources/json/abc1.json @@ -0,0 +1,74 @@ +//老版本的订阅回调 +{ + "auto_renew_status_change_date_pst": "2025-05-20 19:49:10 America/Los_Angeles", + "auto_renew_status": "false", + "original_transaction_id": 70002493746700, + "unified_receipt": { + "latest_receipt": "MIIWIgYJKoZIhvcNAQcCoIIWEzCCFg8CAQExDzANBglghkgBZQMEAgEFADCCBVgGCSqGSIb3DQEHAaCCBUkEggVFMYIFQTAKAgETAgEBBAIMADAKAgEUAgEBBAIMADALAgEZAgEBBAMCAQMwDAIBDgIBAQQEAgIA5zANAgENAgEBBAUCAwK/hTAOAgEBAgEBBAYCBFpTRkswDgIBAwIBAQQGDAQxMTg4MA4CAQkCAQEEBgIEUDMwNTAOAgEKAgEBBAYWBG5vbmUwDgIBCwIBAQQGAgQHXoWLMA4CARACAQEEBgIENB4QxTAUAgEAAgEBBAwMClByb2R1Y3Rpb24wFwIBAgIBAQQPDA1jb20uZXBhbC5lcGFsMBgCAQQCAQIEEOVw631eZnZbHkxrT6JcHi8wHAIBBQIBAQQUmve43pSb5n3kaoPbPHtCr5XQlmkwHgIBCAIBAQQWFhQyMDI1LTAyLTIyVDExOjQ4OjI4WjAeAgEMAgEBBBYWFDIwMjUtMDUtMjFUMDI6NDk6MTZaMB4CARICAQEEFhYUMjAyNS0wMi0yMlQxMTo0ODoyOFowTQIBBgIBAQRF5P3J1PvUX8c7alI46ePhk96/YG4YRRV9WFnjru/VJX7mQSUwAkWdaXSXZ/LQFnATQEYKEiyAGxglKClVgbGHjeKLO8idMFECAQcCAQEESSzNIuHPOQ7pv0VbMrTzOuL2nfruskFzs07/ViUSMCi5f2bWreWlCQ/GIAih70xr2cnxe6FoeFMbyQiGGdmiLC+Nn0zuYfBjvCswggGXAgERAgEBBIIBjTGCAYkwCwICBq0CAQEEAgwAMAsCAgawAgEBBAIWADALAgIGsgIBAQQCDAAwCwICBrMCAQEEAgwAMAsCAga0AgEBBAIMADALAgIGtQIBAQQCDAAwCwICBrYCAQEEAgwAMAwCAgalAgEBBAMCAQEwDAICBqsCAQEEAwIBAzAMAgIGsQIBAQQDAgEAMAwCAga3AgEBBAMCAQAwDAICBroCAQEEAwIBADAPAgIGrgIBAQQGAgRhDTZoMBECAgavAgEBBAgCBj+qaaGU4jAZAgIGpwIBAQQQDA43MDAwMjQ5Mzc0NjcwMDAZAgIGqQIBAQQQDA43MDAwMjQ5Mzc0NjcwMDAfAgIGqAIBAQQWFhQyMDI1LTAxLTEzVDA4OjM4OjM4WjAfAgIGqgIBAQQWFhQyMDI1LTAxLTEzVDA4OjM4OjQwWjAfAgIGrAIBAQQWFhQyMDI1LTAyLTEzVDA4OjM4OjM4WjApAgIGpgIBAQQgDB5nZy5lcGFsLmF1dG9yZW5ldy5uZXd2aXAxbW9udGgwggGXAgERAgEBBIIBjTGCAYkwCwICBq0CAQEEAgwAMAsCAgawAgEBBAIWADALAgIGsgIBAQQCDAAwCwICBrMCAQEEAgwAMAsCAga0AgEBBAIMADALAgIGtQIBAQQCDAAwCwICBrYCAQEEAgwAMAwCAgalAgEBBAMCAQEwDAICBqsCAQEEAwIBAzAMAgIGsQIBAQQDAgEAMAwCAga3AgEBBAMCAQAwDAICBroCAQEEAwIBADAPAgIGrgIBAQQGAgRhDTZoMBECAgavAgEBBAgCBj+qaaGU4zAZAgIGpwIBAQQQDA43MDAwMjU2NTI3ODc0NjAZAgIGqQIBAQQQDA43MDAwMjQ5Mzc0NjcwMDAfAgIGqAIBAQQWFhQyMDI1LTAyLTIyVDExOjQ4OjI4WjAfAgIGqgIBAQQWFhQyMDI1LTAxLTEzVDA4OjM4OjQwWjAfAgIGrAIBAQQWFhQyMDI1LTAzLTIyVDEwOjQ4OjI4WjApAgIGpgIBAQQgDB5nZy5lcGFsLmF1dG9yZW5ldy5uZXd2aXAxbW9udGiggg7iMIIFxjCCBK6gAwIBAgIQfTkgCU6+8/jvymwQ6o5DAzANBgkqhkiG9w0BAQsFADB1MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzUxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTI0MDcyNDE0NTAwM1oXDTI2MDgyMzE0NTAwMlowgYkxNzA1BgNVBAMMLk1hYyBBcHAgU3RvcmUgYW5kIGlUdW5lcyBTdG9yZSBSZWNlaXB0IFNpZ25pbmcxLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVsYXRpb25zMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK0PNpvPN9qBcVvW8RT8GdP11PA3TVxGwpopR1FhvrE/mFnsHBe6r7MJVwVE1xdtXdIwwrszodSJ9HY5VlctNT9NqXiC0Vph1nuwLpVU8Ae/YOQppDM9R692j10Dm5o4CiHM3xSXh9QdYcoqjcQ+Va58nWIAsAoYObjmHY3zpDDxlJNj2xPpPI4p/dWIc7MUmG9zyeIz1Sf2tuN11urOq9/i+Ay+WYrtcHqukgXZTAcg5W1MSHTQPv5gdwF5PhM7f4UAz5V/gl2UIDTrknW1BkH7n5mXJLrvutiZSvR3LnnYON6j2C9FUETkMyKZ1fflnIT5xgQRy+BV4TTLFbIjFaUCAwEAAaOCAjswggI3MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUGYuXjUpbYXhX9KVcNRKKOQjjsHUwcAYIKwYBBQUHAQEEZDBiMC0GCCsGAQUFBzAChiFodHRwOi8vY2VydHMuYXBwbGUuY29tL3d3ZHJnNS5kZXIwMQYIKwYBBQUHMAGGJWh0dHA6Ly9vY3NwLmFwcGxlLmNvbS9vY3NwMDMtd3dkcmc1MDUwggEfBgNVHSAEggEWMIIBEjCCAQ4GCiqGSIb3Y2QFBgEwgf8wNwYIKwYBBQUHAgEWK2h0dHBzOi8vd3d3LmFwcGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eS8wgcMGCCsGAQUFBwICMIG2DIGzUmVsaWFuY2Ugb24gdGhpcyBjZXJ0aWZpY2F0ZSBieSBhbnkgcGFydHkgYXNzdW1lcyBhY2NlcHRhbmNlIG9mIHRoZSB0aGVuIGFwcGxpY2FibGUgc3RhbmRhcmQgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YgdXNlLCBjZXJ0aWZpY2F0ZSBwb2xpY3kgYW5kIGNlcnRpZmljYXRpb24gcHJhY3RpY2Ugc3RhdGVtZW50cy4wMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL2NybC5hcHBsZS5jb20vd3dkcmc1LmNybDAdBgNVHQ4EFgQU7yhXtGCISVUx8P1YDvH9GpPEJPwwDgYDVR0PAQH/BAQDAgeAMBAGCiqGSIb3Y2QGCwEEAgUAMA0GCSqGSIb3DQEBCwUAA4IBAQA1I9K7UL82Z8wANUR8ipOnxF6fuUTqckfPEIa6HO0KdR5ZMHWFyiJ1iUIL4Zxw5T6lPHqQ+D8SrHNMJFiZLt+B8Q8lpg6lME6l5rDNU3tFS7DmWzow1rT0K1KiD0/WEyOCM+YthZFQfDHUSHGU+giV7p0AZhq55okMjrGJfRZKsIgVHRQphxQdMfquagDyPZFjW4CCSB4+StMC3YZdzXLiNzyoCyW7Y9qrPzFlqCcb8DtTRR0SfkYfxawfyHOcmPg0sGB97vMRDFaWPgkE5+3kHkdZsPCDNy77HMcTo2ly672YJpCEj25N/Ggp+01uGO3craq5xGmYFAj9+Uv7bP6ZMIIEVTCCAz2gAwIBAgIUO36ACu7TAqHm7NuX2cqsKJzxaZQwDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsTHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBSb290IENBMB4XDTIwMTIxNjE5Mzg1NloXDTMwMTIxMDAwMDAwMFowdTFEMEIGA1UEAww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxCzAJBgNVBAsMAkc1MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ9d2h/7+rzQSyI8x9Ym+hf39J8ePmQRZprvXr6rNL2qLCFu1h6UIYUsdMEOEGGqPGNKfkrjyHXWz8KcCEh7arkpsclm/ciKFtGyBDyCuoBs4v8Kcuus/jtvSL6eixFNlX2ye5AvAhxO/Em+12+1T754xtress3J2WYRO1rpCUVziVDUTuJoBX7adZxLAa7a489tdE3eU9DVGjiCOtCd410pe7GB6iknC/tgfIYS+/BiTwbnTNEf2W2e7XPaeCENnXDZRleQX2eEwXN3CqhiYraucIa7dSOJrXn25qTU/YMmMgo7JJJbIKGc0S+AGJvdPAvntf3sgFcPF54/K4cnu/cCAwEAAaOB7zCB7DASBgNVHRMBAf8ECDAGAQH/AgEAMB8GA1UdIwQYMBaAFCvQaUeUdgn+9GuNLkCm90dNfwheMEQGCCsGAQUFBwEBBDgwNjA0BggrBgEFBQcwAYYoaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwMy1hcHBsZXJvb3RjYTAuBgNVHR8EJzAlMCOgIaAfhh1odHRwOi8vY3JsLmFwcGxlLmNvbS9yb290LmNybDAdBgNVHQ4EFgQUGYuXjUpbYXhX9KVcNRKKOQjjsHUwDgYDVR0PAQH/BAQDAgEGMBAGCiqGSIb3Y2QGAgEEAgUAMA0GCSqGSIb3DQEBCwUAA4IBAQBaxDWi2eYKnlKiAIIid81yL5D5Iq8UJcyqCkJgksK9dR3rTMoV5X5rQBBe+1tFdA3wen2Ikc7eY4tCidIY30GzWJ4GCIdI3UCvI9Xt6yxg5eukfxzpnIPWlF9MYjmKTq4TjX1DuNxerL4YQPLmDyxdE5Pxe2WowmhI3v+0lpsM+zI2np4NlV84CouW0hJst4sLjtc+7G8Bqs5NRWDbhHFmYuUZZTDNiv9FU/tu+4h3Q8NIY/n3UbNyXnniVs+8u4S5OFp4rhFIUrsNNYuU3sx0mmj1SWCUrPKosxWGkNDMMEOG0+VwAlG0gcCol9Tq6rCMCUDvOJOyzSID62dDZchFMIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg++FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9wtj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IWq6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKMaLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAEggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBcNplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQPy3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4FgxhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oPIQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AXUKqK1drk/NAJBzewdXUhMYIBtTCCAbECAQEwgYkwdTFEMEIGA1UEAww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxCzAJBgNVBAsMAkc1MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUwIQfTkgCU6+8/jvymwQ6o5DAzANBglghkgBZQMEAgEFADANBgkqhkiG9w0BAQEFAASCAQAUbYqsFQf99xqh6dIymTZf87NxwPUUyRddAoSSoNmKIt9Mh9D1zq39KkDzhpdDLw2SspOmQD/8TcYmCzBUuocpwyaytArFLViFdafZXh1Li8R8FavkYligzWOY5fmIrmM7pHLmGSw3BuRs7DiPwszlP6II8YmDXFiEjJRKz+hn90Y5itMXzDgjKW659jBheRh43tjy1ZwBSvs10ky31QPLIllvdISE+m27mB0g8oqBDN7AK2aFLL/xQ7KmRB+D1SYgIIi3suXlIRFoa21GCrnXb0zDHawKiX5cxQU0JFPjPv5L1FfsU1A+bkV/FQWjJwUp1LPxgiUQcEJreLvuWfhB", + "pending_renewal_info": [ + { + "is_in_billing_retry_period": "0", + "auto_renew_status": "0", + "original_transaction_id": "70002493746700", + "product_id": "gg.epal.autorenew.newvip1month", + "expiration_intent": "2", + "auto_renew_product_id": "gg.epal.autorenew.newvip1month" + } + ], + "environment": "Production", + "status": 0, + "latest_receipt_info": [ + { + "expires_date_pst": "2025-03-22 03:48:28 America/Los_Angeles", + "purchase_date": "2025-02-22 11:48:28 Etc/GMT", + "in_app_ownership_type": "PURCHASED", + "purchase_date_ms": "1740224908000", + "original_purchase_date_ms": "1736757520000", + "transaction_id": "70002565278746", + "original_transaction_id": "70002493746700", + "quantity": "1", + "expires_date_ms": "1742640508000", + "original_purchase_date_pst": "2025-01-13 00:38:40 America/Los_Angeles", + "product_id": "gg.epal.autorenew.newvip1month", + "subscription_group_identifier": "20962890", + "web_order_line_item_id": "70001149187299", + "expires_date": "2025-03-22 10:48:28 Etc/GMT", + "is_in_intro_offer_period": "false", + "original_purchase_date": "2025-01-13 08:38:40 Etc/GMT", + "purchase_date_pst": "2025-02-22 03:48:28 America/Los_Angeles", + "is_trial_period": "false" + }, + { + "expires_date_pst": "2025-02-13 00:38:38 America/Los_Angeles", + "purchase_date": "2025-01-13 08:38:38 Etc/GMT", + "in_app_ownership_type": "PURCHASED", + "purchase_date_ms": "1736757518000", + "original_purchase_date_ms": "1736757520000", + "transaction_id": "70002493746700", + "original_transaction_id": "70002493746700", + "quantity": "1", + "expires_date_ms": "1739435918000", + "original_purchase_date_pst": "2025-01-13 00:38:40 America/Los_Angeles", + "product_id": "gg.epal.autorenew.newvip1month", + "subscription_group_identifier": "20962890", + "web_order_line_item_id": "70001149187298", + "expires_date": "2025-02-13 08:38:38 Etc/GMT", + "is_in_intro_offer_period": "false", + "original_purchase_date": "2025-01-13 08:38:40 Etc/GMT", + "purchase_date_pst": "2025-01-13 00:38:38 America/Los_Angeles", + "is_trial_period": "false" + } + ] + }, + "auto_renew_status_change_date_ms": "1747795750000", + "auto_renew_product_id": "gg.epal.autorenew.newvip1month", + "notification_type": "DID_CHANGE_RENEWAL_STATUS", + "auto_renew_status_change_date": "2025-05-21 02:49:10 Etc/GMT", + "environment": "PROD", + "deprecation": "Mon, 5 Jun 2023 23:59:59 GMT", + "bvrs": "1188", + "password": "d66b6c9531394883a3732526c46d4aca", + "bid": "com.epal.epal" +} + + diff --git a/sonic-lion/server/src/main/resources/json/abc2.json b/sonic-lion/server/src/main/resources/json/abc2.json new file mode 100644 index 0000000..fd2acac --- /dev/null +++ b/sonic-lion/server/src/main/resources/json/abc2.json @@ -0,0 +1,32 @@ +//内购 内购回调 +{ + "notificationType": "ONE_TIME_CHARGE", + "notificationUUID": "bd447a0e-f2e2-4d3b-a36a-b542f06bd381", + "data": { + "appAppleId": 6745584732, + "bundleId": "gg.epal.es", + "bundleVersion": "102", + "environment": "Sandbox", + "signedTransactionInfo": { + "transactionId": "2000000924298472", + "originalTransactionId": "2000000924298472", + "bundleId": "gg.epal.es", + "productId": "gg.epal.es.buff_0006", + "purchaseDate": 1747899274000, + "originalPurchaseDate": 1747899274000, + "quantity": 1, + "type": "Consumable", + "inAppOwnershipType": "PURCHASED", + "signedDate": 1747899280949, + "environment": "Sandbox", + "transactionReason": "PURCHASE", + "storefront": "GBR", + "storefrontId": "143444", + "price": 39990, + "currency": "GBP", + "appTransactionId": "704523959015915218" + } + }, + "version": "2.0", + "signedDate": 1747899280978 +} diff --git a/sonic-lion/server/src/main/resources/logback-spring.xml b/sonic-lion/server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b8dd4d1 --- /dev/null +++ b/sonic-lion/server/src/main/resources/logback-spring.xml @@ -0,0 +1,62 @@ + + + + + + + + + + ${DefaultPattern} + + + + + + + + ${ColorfulPattern} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sonic-lion/server/src/main/resources/main/api-google.p12 b/sonic-lion/server/src/main/resources/main/api-google.p12 new file mode 100644 index 0000000..13a2e8a Binary files /dev/null and b/sonic-lion/server/src/main/resources/main/api-google.p12 differ diff --git a/sonic-lion/server/src/main/resources/mapper/AccountBuffAwaitingMapper.xml b/sonic-lion/server/src/main/resources/mapper/AccountBuffAwaitingMapper.xml new file mode 100644 index 0000000..8f96818 --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/AccountBuffAwaitingMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/AccountBuffBillMapper.xml b/sonic-lion/server/src/main/resources/mapper/AccountBuffBillMapper.xml new file mode 100644 index 0000000..169b8aa --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/AccountBuffBillMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/AccountBuffMapper.xml b/sonic-lion/server/src/main/resources/mapper/AccountBuffMapper.xml new file mode 100644 index 0000000..c516d9e --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/AccountBuffMapper.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + update account_buff + set awaiting_income = awaiting_income + #{buff} + where id = #{id} + + + + update account_buff + set balance = balance + #{buff} + where id = #{id} + + + + update account_buff + set withdrawable_income = withdrawable_income + #{buff} + where id = #{id} + + + + update account_buff + set withdrawable_income = withdrawable_income - #{buff} + where id = #{id} + and withdrawable_income >= #{buff} + + + + update account_buff + set withdrawable_income = withdrawable_income - #{buff}, + frozen_income = frozen_income + #{buff} + where id = #{id} + and frozen_income >= #{buff} + + + + update account_buff + set frozen_income = frozen_income - #{buff} + where id = #{id} + and frozen_income >= #{buff} + + + + update account_buff + set awaiting_income = awaiting_income - #{buff}, + withdrawable_income = withdrawable_income + #{buff} + where id = #{id} + and awaiting_income >= #{buff} + + + + update account_buff + set awaiting_income = awaiting_income - #{buff} + where id = #{id} + and awaiting_income >= #{buff} + + + + update account_buff + set balance = balance - #{buff} + where id = #{id} + and balance >= #{buff} + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/GoogleUploadReceiptMapper.xml b/sonic-lion/server/src/main/resources/mapper/GoogleUploadReceiptMapper.xml new file mode 100644 index 0000000..8be4efc --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/GoogleUploadReceiptMapper.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/IapUploadReceiptMapper.xml b/sonic-lion/server/src/main/resources/mapper/IapUploadReceiptMapper.xml new file mode 100644 index 0000000..9561330 --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/IapUploadReceiptMapper.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/MemberPrivDictMapper.xml b/sonic-lion/server/src/main/resources/mapper/MemberPrivDictMapper.xml new file mode 100644 index 0000000..c43a742 --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/MemberPrivDictMapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + id, title, `desc`, img, sort, is_delete, create_time + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/PayAccountFundAwaitingMapper.xml b/sonic-lion/server/src/main/resources/mapper/PayAccountFundAwaitingMapper.xml new file mode 100644 index 0000000..ef62d23 --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/PayAccountFundAwaitingMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/PayAccountFundFrozenMapper.xml b/sonic-lion/server/src/main/resources/mapper/PayAccountFundFrozenMapper.xml new file mode 100644 index 0000000..9da697d --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/PayAccountFundFrozenMapper.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + update t_pay_account_fund_frozen + set freeze_status = 2, + unfreeze_date = now() + where id = #{id} + and freeze_status = 1 + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/PayAccountFundMapper.xml b/sonic-lion/server/src/main/resources/mapper/PayAccountFundMapper.xml new file mode 100644 index 0000000..9ca9600 --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/PayAccountFundMapper.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + update t_pay_account_fund + set awaiting_amount = awaiting_amount + #{amount} + where account_id = #{accountId} + and channel_id = #{payChannel.value} + + + update t_pay_account_fund + set charge = charge + #{amount} + where account_id = #{accountId} + and channel_id = #{payChannel.value} + + + + update t_pay_account_fund + set balance = balance + #{amount} + where account_id = #{accountId} + and channel_id = #{payChannel.value} + + + + update t_pay_account_fund + set balance = balance - #{amount} + where account_id = #{accountId} + and channel_id = #{payChannel.value} + and balance >= #{amount} + + + + update t_pay_account_fund + set balance = balance - #{amount}, + frozen_amount = frozen_amount + #{amount} + where id = #{id} + and balance >= #{amount} + + + + update t_pay_account_fund + set frozen_amount = frozen_amount - #{amount} + where id = #{id} + and frozen_amount >= #{amount} + + + + update t_pay_account_fund + set awaiting_amount = awaiting_amount - #{amount}, + balance = balance + #{amount} + where account_id = #{accountId} + and channel_id = #{payChannel.value} + and awaiting_amount >= #{amount} + + + + update t_pay_account_fund + set awaiting_amount = awaiting_amount - #{amount} + where account_id = #{accountId} + and channel_id = #{payChannel.value} + and awaiting_amount >= #{amount} + + + update t_pay_account_fund + set charge = charge - #{amount} + where account_id = #{accountId} + and channel_id = #{payChannel.value} + and charge >= #{amount} + + + + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/PayAccountFundThirdMapper.xml b/sonic-lion/server/src/main/resources/mapper/PayAccountFundThirdMapper.xml new file mode 100644 index 0000000..f01409b --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/PayAccountFundThirdMapper.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/PayCallChannelRecordMapper.xml b/sonic-lion/server/src/main/resources/mapper/PayCallChannelRecordMapper.xml new file mode 100644 index 0000000..4a490d9 --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/PayCallChannelRecordMapper.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + update t_pay_call_channel_record set status = #{status} where id = #{id} + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/PayTradeMapper.xml b/sonic-lion/server/src/main/resources/mapper/PayTradeMapper.xml new file mode 100644 index 0000000..b37366c --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/PayTradeMapper.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + update t_pay_trade + set status = 5, + status_in_time = now() + where status = 1 + and close_time < now() + + + + + + + + + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/mapper/UserSubscriptionMapper.xml b/sonic-lion/server/src/main/resources/mapper/UserSubscriptionMapper.xml new file mode 100644 index 0000000..112cbc3 --- /dev/null +++ b/sonic-lion/server/src/main/resources/mapper/UserSubscriptionMapper.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/messages.properties b/sonic-lion/server/src/main/resources/messages.properties new file mode 100644 index 0000000..4d5ffb2 --- /dev/null +++ b/sonic-lion/server/src/main/resources/messages.properties @@ -0,0 +1,49 @@ +MISS_PARAM_ERROR=Missing parameter +STATUS_ERROR=Status error +EXISTS_ERROR=Data already exists +SYSTEM_EXCEPTION=System error +IP_EXCEPTION=System error +PARAM_ERROR=Parameter error +PARAM_BLANK=Parameter cannot be empty +NO_THIRD_PARTY_ACCOUNT=Third-party account does not exist +THIRD_PARTY_ACCOUNT_UNBIND=Third-party account is not bound +DATA_NOT_EXITS=Data does not exist +OUT_TRADE_NO_EMPTY=Out trade number is empty +DATA_STATUS_INCORRECT=Data status error +DATA_STATUS_CHANGED=Data status has changed, please refresh and try again +PLATFORM_PAYPAL=Source must be PayPal +PAY_TRADE_STATUS_CHANGE=Transaction status has changed +AMOUNT_LESS_THAN0=Paid amount cannot be less than 0 +AMOUNT_LESS_THAN_N=Paid amount cannot be less than N +PAY_TRADE_PAYER_PAYEE_ERROR=Transaction parties cannot be the same +PAY_TRADE_PAYER_ERROR=This trade is not for you +PAY_TRADE_PAYMENT_TYPE_ERROR=Payment type error +SUBSCRIPTION_STATUS_ERROR=PayPal subscription status error +USER_ID_NOT_NULL=Bound user ID cannot be empty +GAME_PAY_FAIL=Game order payment failed +INSUFFICIENT_BALANCE=Insufficient balance +INSUFFICIENT_BALANCE_WHEN_ORDER_FAIL=Insufficient balance +GOOGLE_TICKET_STATUS_ERROR=Incorrect ticket status +REVEIVE_FAIL=Conditions for claiming not met +REWARD_REVEIVED=Reward already claimed +IAP_RECEIPT_STATUS_ERROR=Receipt is not in a successful state +IAP_BUNDLE_ID_ERROR=Receipt application ID does not match +DUPLICATE_ORDER_NUMBER=Duplicate order number +UNSUPPORTED_BUSINESS_TYPE=Unsupported business type +IN_PROCESSING=Transaction processing. Please refresh and try again later +RESOURCEKEY_IS_BLANK=resourceKey is blank +RESOURCENUM_EXCEEDS_THE_MAXIMUM=resourceNum exceeds the maximum +TRADE_NO_BLANK=tradeNo cannot be empty +TURN_ON_OFF=Switch is not enabled +KEY_ERROR=Key error +AUTH_ERROR=Authentication error +LIMIT_ERROR=Limit error +CHARGE_CANCEL=Charge canceled +CHARGE_EXPIRED=Charge expired +CHARGE_FAIL=Charge failed +SYS_PERMISSION_DENIED=Insufficient permissions +CHANNEL_BLACK_ERROR=The current payment method is not supported for payment. Please use another payment method +AIRWALLEX_CREATE_USER_ERROR=Airwallex create user error +CHANNEL_NOT_OPEN=Channel not open +SUB_PRODUCT_NOT_FOUND=Subscription product not found +SUBSCRIBED_NO_DUPLICATE=Subscription active, duplicate subscription not allowed \ No newline at end of file diff --git a/sonic-lion/server/src/main/resources/messages_en.properties b/sonic-lion/server/src/main/resources/messages_en.properties new file mode 100644 index 0000000..4d5ffb2 --- /dev/null +++ b/sonic-lion/server/src/main/resources/messages_en.properties @@ -0,0 +1,49 @@ +MISS_PARAM_ERROR=Missing parameter +STATUS_ERROR=Status error +EXISTS_ERROR=Data already exists +SYSTEM_EXCEPTION=System error +IP_EXCEPTION=System error +PARAM_ERROR=Parameter error +PARAM_BLANK=Parameter cannot be empty +NO_THIRD_PARTY_ACCOUNT=Third-party account does not exist +THIRD_PARTY_ACCOUNT_UNBIND=Third-party account is not bound +DATA_NOT_EXITS=Data does not exist +OUT_TRADE_NO_EMPTY=Out trade number is empty +DATA_STATUS_INCORRECT=Data status error +DATA_STATUS_CHANGED=Data status has changed, please refresh and try again +PLATFORM_PAYPAL=Source must be PayPal +PAY_TRADE_STATUS_CHANGE=Transaction status has changed +AMOUNT_LESS_THAN0=Paid amount cannot be less than 0 +AMOUNT_LESS_THAN_N=Paid amount cannot be less than N +PAY_TRADE_PAYER_PAYEE_ERROR=Transaction parties cannot be the same +PAY_TRADE_PAYER_ERROR=This trade is not for you +PAY_TRADE_PAYMENT_TYPE_ERROR=Payment type error +SUBSCRIPTION_STATUS_ERROR=PayPal subscription status error +USER_ID_NOT_NULL=Bound user ID cannot be empty +GAME_PAY_FAIL=Game order payment failed +INSUFFICIENT_BALANCE=Insufficient balance +INSUFFICIENT_BALANCE_WHEN_ORDER_FAIL=Insufficient balance +GOOGLE_TICKET_STATUS_ERROR=Incorrect ticket status +REVEIVE_FAIL=Conditions for claiming not met +REWARD_REVEIVED=Reward already claimed +IAP_RECEIPT_STATUS_ERROR=Receipt is not in a successful state +IAP_BUNDLE_ID_ERROR=Receipt application ID does not match +DUPLICATE_ORDER_NUMBER=Duplicate order number +UNSUPPORTED_BUSINESS_TYPE=Unsupported business type +IN_PROCESSING=Transaction processing. Please refresh and try again later +RESOURCEKEY_IS_BLANK=resourceKey is blank +RESOURCENUM_EXCEEDS_THE_MAXIMUM=resourceNum exceeds the maximum +TRADE_NO_BLANK=tradeNo cannot be empty +TURN_ON_OFF=Switch is not enabled +KEY_ERROR=Key error +AUTH_ERROR=Authentication error +LIMIT_ERROR=Limit error +CHARGE_CANCEL=Charge canceled +CHARGE_EXPIRED=Charge expired +CHARGE_FAIL=Charge failed +SYS_PERMISSION_DENIED=Insufficient permissions +CHANNEL_BLACK_ERROR=The current payment method is not supported for payment. Please use another payment method +AIRWALLEX_CREATE_USER_ERROR=Airwallex create user error +CHANNEL_NOT_OPEN=Channel not open +SUB_PRODUCT_NOT_FOUND=Subscription product not found +SUBSCRIBED_NO_DUPLICATE=Subscription active, duplicate subscription not allowed \ No newline at end of file diff --git a/sonic-pigeon/.gitignore b/sonic-pigeon/.gitignore new file mode 100644 index 0000000..51bb6a0 --- /dev/null +++ b/sonic-pigeon/.gitignore @@ -0,0 +1,26 @@ +/**/rebel.xml + +#target/ +target/ + +# IDEA # +.idea/ +*.iml + +# Eclipse # +.settings/ +.classpath +.project + +# Log # +logs/ +*.log% + +#mac +.DS_Store + +#svn +.svn +#reviewboardrc +.reviewboardrc +**/rest-client.private.env.json diff --git a/sonic-pigeon/README.md b/sonic-pigeon/README.md new file mode 100644 index 0000000..1570110 --- /dev/null +++ b/sonic-pigeon/README.md @@ -0,0 +1,3 @@ +# sonic-pigeon + +消息系统 \ No newline at end of file diff --git a/sonic-pigeon/common/pom.xml b/sonic-pigeon/common/pom.xml new file mode 100644 index 0000000..79f24bc --- /dev/null +++ b/sonic-pigeon/common/pom.xml @@ -0,0 +1,54 @@ + + + + sonic-pigeon + com.sonic.pigeon + 1.0 + + 4.0.0 + + sonic-pigeon-common + jar + 1.0-SNAPSHOT + + + + com.sonic + common-lib + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + mysql + mysql-connector-java + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.google.guava + guava + + + com.alibaba + fastjson + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-pool2 + + + diff --git a/sonic-pigeon/common/src/main/java/com/sonic/pigeon/common/GlobalConfig.java b/sonic-pigeon/common/src/main/java/com/sonic/pigeon/common/GlobalConfig.java new file mode 100644 index 0000000..f7dab05 --- /dev/null +++ b/sonic-pigeon/common/src/main/java/com/sonic/pigeon/common/GlobalConfig.java @@ -0,0 +1,106 @@ +package com.sonic.pigeon.common; + +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.AppRuntime; +import com.sonic.common.log.RequestLogAspect; +import com.sonic.common.rpc.HttpClient; +import com.sonic.common.rpc.RpcClient; +import com.sonic.common.rpc.RpcClientImpl; +import com.sonic.common.stat.FrequencyAlertInterceptor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.task.TaskExecutorBuilder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.stream.Collectors; + +import static org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME; + +/** + * @author coder + */ +@Slf4j +public class GlobalConfig { + @Value("${spring.application.name}") + private String appName; + @Value("${spring.application.id}") + private String appId; + + @Bean + ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() { + return new ScheduledThreadPoolExecutor(1); + } + + /** + * XXX Spring默认配置当有Executor的bean后不再装载TaskExecutor, 这里因为手动注册了 + * {@link #scheduledThreadPoolExecutor}会导致spring不再自动注册TaskExecutor, 因此需要去掉ConditionalOnMissingBean的限制. + * 参考{@link TaskExecutionAutoConfiguration#applicationTaskExecutor} + */ + @Bean(name = {APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME}) + public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { + return builder.build(); + } + + @Bean + public RequestLogAspect requestLogAspect() { + return new RequestLogAspect(); + } + + @Bean + AppRuntime appRuntime(ApplicationContext applicationContext) { + return AppRuntime.builder().appId(appId).appName(appName).applicationContext(applicationContext).build(); + } + + /** + * 用于配置接口访问频率超限告警, 接口频率配置参考application.yml中的web.frequency-alert.rules + * TODO 根据需要配置为输出到日志或者alertClient + * + * @return + */ + @Bean + public FrequencyAlertInterceptor frequencyAlertInterceptor(RuleConfig ruleConfig) { + return new FrequencyAlertInterceptor(ruleConfig.getAlertRuleMap(), + (request, frequencyAlert) -> log.warn("frequency alert, api frequency alert, url = {} alert = {}", + request.getRequestURI(), JSONObject.toJSONString(frequencyAlert))); + } + + @Bean + @Primary + public RpcClient rpcClient() { + return new RpcClientImpl(HttpClient.Config.EMPTY); + } + + @Data + @Component + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "web.frequency-alert") + public static class RuleConfig { + private List rules; + + public Map getAlertRuleMap() { + if (rules == null) { + return Collections.emptyMap(); + } + return rules.stream() + .map(e -> { + String[] split = e.split(":"); + return new AbstractMap.SimpleEntry<>(split[0], split[1]); + }).collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); + } + } +} diff --git a/sonic-pigeon/common/src/main/java/com/sonic/pigeon/common/MybatisPlusConfig.java b/sonic-pigeon/common/src/main/java/com/sonic/pigeon/common/MybatisPlusConfig.java new file mode 100644 index 0000000..cd6c7b1 --- /dev/null +++ b/sonic-pigeon/common/src/main/java/com/sonic/pigeon/common/MybatisPlusConfig.java @@ -0,0 +1,19 @@ +package com.sonic.pigeon.common; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; + +@EnableTransactionManagement +@MapperScan("com.sonic.**.dao") +public class MybatisPlusConfig { + + @Bean + public PaginationInterceptor paginationInterceptor() { + PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); + paginationInterceptor.setDialectType("mysql"); + return paginationInterceptor; + } +} diff --git a/sonic-pigeon/lib/pom.xml b/sonic-pigeon/lib/pom.xml new file mode 100644 index 0000000..79c7412 --- /dev/null +++ b/sonic-pigeon/lib/pom.xml @@ -0,0 +1,47 @@ + + + + sonic-pigeon + com.sonic.pigeon + 1.0 + + 4.0.0 + + com.sonic.pigeon + sonic-pigeon-lib + jar + 1.1-SNAPSHOT + + + + + com.sonic + common-lib + + + + log4j-api + org.apache.logging.log4j + + + com.alibaba + fastjson + + + + + + com.sonic + common-lib + + + + com.github.ben-manes.caffeine + caffeine + + + + + \ No newline at end of file diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/bo/AttachBo.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/bo/AttachBo.java new file mode 100644 index 0000000..ad72faf --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/bo/AttachBo.java @@ -0,0 +1,38 @@ +package com.sonic.pigeon.lib.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class AttachBo { + + /** + * 类型 image + */ + private String type; + + /** + * 图片地址 + */ + private String url; + + /** + * 解锁价格 + */ + private Long unlockPrice; + + /** + * 相册id + */ + private Long albumId; + + private String width; + + private String height; + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/bo/ScoreExtension.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/bo/ScoreExtension.java new file mode 100644 index 0000000..da9d4d4 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/bo/ScoreExtension.java @@ -0,0 +1,19 @@ +package com.sonic.pigeon.lib.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ScoreExtension { + /** + * 打分分值 + */ + private BigDecimal score; +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/ImConversationClient.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/ImConversationClient.java new file mode 100644 index 0000000..d33351a --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/ImConversationClient.java @@ -0,0 +1,64 @@ +package com.sonic.pigeon.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.pigeon.lib.input.CreateConversationInput; +import com.sonic.pigeon.lib.input.UpdateConversationInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author code + */ +@Slf4j +@Service +public class ImConversationClient { + + private static final String IM_CONVERSATION_CREATE = "/api/im-conversation/create"; + private static final String IM_CONVERSATION_UPDATE = "/api/im-conversation/update"; + + private RpcClient rpcClient; + private String host; + + public ImConversationClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-pigeon-svc:8080"; + break; + case product: + default: + this.host = "http://prod-pigeon-svc:8080"; + } + } + + + + + /** + * 新建会话扩展字段 + * @param input + * @return + */ + public void createConversation(CreateConversationInput input) { + rpcClient.post(host + IM_CONVERSATION_CREATE, input, new TypeReference>(){}); + } + + /** + * 更新会话扩展字段 + * @param input + * @return + */ + public void updateConversation(UpdateConversationInput input) { + rpcClient.post(host + IM_CONVERSATION_UPDATE, input, new TypeReference>(){}); + } + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/ImMessageClient.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/ImMessageClient.java new file mode 100644 index 0000000..7564528 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/ImMessageClient.java @@ -0,0 +1,131 @@ +package com.sonic.pigeon.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.pigeon.lib.input.HistoryMessageInput; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import com.sonic.pigeon.lib.input.SendAiTextMessageInput; +import com.sonic.pigeon.lib.input.UpdateAiSendCustomImageMessageInput; +import com.sonic.pigeon.lib.output.HistoryMessageOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author code + */ +@Slf4j +@Service +public class ImMessageClient { + + private static final String SEND_AI_TO_USER_TEXT_MESSAGE = "/api/im-message/send-ai-to-user-text-message"; + + private static final String SEND_USER_TO_AI_TEXT_MESSAGE = "/api/im-message/send-user-to-ai-text-message"; + + private static final String SEND_AI_TO_USER_CUSTOMER_MESSAGE = "/api/im-message/send-ai-to-user-customer-message"; + + private static final String SEND_USER_TO_AI_CUSTOMER_MESSAGE = "/api/im-message/send-user-to-ai-customer-message"; + + private static final String GET_HISTORY_MESSAGE = "/api/im-message/get-history-message"; + + private static final String GET_AI_TO_USER_HISTORY_MESSAGE = "/api/im-message/get-ai-to-user-history-message"; + + private static final String UPDATE_AI_CUSTOM_IMAGE_MESSAGE = "/api/im-message/update-ai-custom-image-message"; + + private RpcClient rpcClient; + private String host; + + public ImMessageClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-pigeon-svc:8080"; + break; + case product: + default: + this.host = "http://prod-pigeon-svc:8080"; + } + } + + + /** + * 发送AI消息 + * @param input + * @return + */ + public void sendAiToUserTextMessage(SendAiTextMessageInput input) { + rpcClient.post(host + SEND_AI_TO_USER_TEXT_MESSAGE, input, new TypeReference>(){}); + } + + /** + * 发送AI消息 + * @param input + * @return + */ + public void sendUserToAiTextMessage(SendAiTextMessageInput input) { + rpcClient.post(host + SEND_USER_TO_AI_TEXT_MESSAGE, input, new TypeReference>(){}); + } + + /** + * 发送AI消息 + * @param input + * @return + */ + public void sendAiToUserCustomerMessage(SendAiCustomerMessageInput input) { + rpcClient.post(host + SEND_AI_TO_USER_CUSTOMER_MESSAGE, input, new TypeReference>(){}); + } + + /** + * 发送AI消息 + * @param input + * @return + */ + public void sendUserToAiCustomerMessage(SendAiCustomerMessageInput input) { + rpcClient.post(host + SEND_USER_TO_AI_CUSTOMER_MESSAGE, input, new TypeReference>(){}); + } + + /** + * 获取历史消息 + * @param input + * @return + */ + public List getHistoryMessage(HistoryMessageInput input) { + return rpcClient.post(host + GET_HISTORY_MESSAGE, input, new TypeReference>>(){}); + } + + /** + * 获取历史消息 + * @param input + * @return + */ + public List getUserToAiHistoryMessage(HistoryMessageInput input) { + return rpcClient.post(host + GET_HISTORY_MESSAGE, input, new TypeReference>>(){}); + } + + /** + * 获取历史消息 + * @param input + * @return + */ + public List getAiToUserHistoryMessage(HistoryMessageInput input) { + return rpcClient.post(host + GET_AI_TO_USER_HISTORY_MESSAGE, input, new TypeReference>>(){}); + } + + /** + * 修改AI发送的自定义图片消息 + * @param input + */ + public void updateAiSendCustomImageMessage(UpdateAiSendCustomImageMessageInput input) { + rpcClient.post(host + UPDATE_AI_CUSTOM_IMAGE_MESSAGE, input, new TypeReference>(){}); + } + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/ImUserClient.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/ImUserClient.java new file mode 100644 index 0000000..b4c621e --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/ImUserClient.java @@ -0,0 +1,63 @@ +package com.sonic.pigeon.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.pigeon.lib.input.CreateImUserInput; +import com.sonic.pigeon.lib.input.EditImUserInput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author code + */ +@Slf4j +@Service +public class ImUserClient { + + private static final String IM_USER_CREATE = "/api/im-user/create"; + + private static final String IM_USER_EDIT = "/api/im-user/edit"; + + private RpcClient rpcClient; + private String host; + + public ImUserClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-pigeon-svc:8080"; + break; + case product: + default: + this.host = "http://prod-pigeon-svc:8080"; + } + } + + + /** + * 创建IM用户 + * @param input + * @return + */ + public void createImUser(CreateImUserInput input) { + rpcClient.post(host + IM_USER_CREATE, input, new TypeReference>(){}); + } + + /** + * 编辑IM用户 + * @param input + * @return + */ + public void editImUser(EditImUserInput input) { + rpcClient.post(host + IM_USER_EDIT, input, new TypeReference>(){}); + } + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/MessageClient.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/MessageClient.java new file mode 100644 index 0000000..7e0b795 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/client/MessageClient.java @@ -0,0 +1,71 @@ +package com.sonic.pigeon.lib.client; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import com.sonic.common.AppRuntime; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.pigeon.lib.input.SendMessageInput; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @Author zzhan + * @Date 2020/10/12 16:24 + * @Version 1.0 + */ +@Service +public class MessageClient { + + private static final String SEND_MESSAGE = "/api/message/send-message"; + private static final String BATCH_SEND_MESSAGE = "/api/message/batch-send-message"; + private static final String SEND_CHATROOM_INVITE = "/api/message/send-chatroom-invite"; + + private RpcClient rpcClient; + private String host; + + public MessageClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://localhost:8080"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-pigeon-svc:8080"; + break; + case product: + default: + this.host = "http://prod-pigeon-svc:8080"; + } + } + + /** + * 发送系统内的通知消息 + * @param req + */ + public void sendMessage(SendMessageInput req) { + try { + rpcClient.postBodySign(host + SEND_MESSAGE, req, new TypeReference>(){}); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 批量发送系统内的通知消息 + */ + public void batchSendMessage(List req) { + try { + rpcClient.postBodySign(host + BATCH_SEND_MESSAGE, req, new TypeReference>() { + }); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/ImMessageTypeEnum.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/ImMessageTypeEnum.java new file mode 100644 index 0000000..c4d479c --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/ImMessageTypeEnum.java @@ -0,0 +1,29 @@ +package com.sonic.pigeon.lib.enums; + +import lombok.Getter; + +@Getter +public enum ImMessageTypeEnum { + //IM发送礼物 + IM_SEND_GIFT("I'm sending you %s %s gift"), + //心动等级升级 + HEARTBEAT_LEVEL_UP("Unlocked \"%s\" title"), + //心动等级降级 + HEARTBEAT_LEVEL_DOWN("Lost \"%s\" title"), + //语音聊天情绪打分 + VOICE_CHAT_EMOTION_SCORE(""), + //余额不足 + INSUFFICIENT_BALANCE("Insufficient balance"), + //语音通话结束 + CALL("Voice call"), + //发送图片 + IMAGE(""), + + ; + + private final String content; + + ImMessageTypeEnum(String content) { + this.content = content; + } +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/ImUserTypeEnum.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/ImUserTypeEnum.java new file mode 100644 index 0000000..8c9eab1 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/ImUserTypeEnum.java @@ -0,0 +1,25 @@ +package com.sonic.pigeon.lib.enums; + +public enum ImUserTypeEnum { + + u("普通用户"), + r("机器人"), + ; + + private String desc; + + ImUserTypeEnum(String desc) { + this.desc = desc; + } + + public static ImUserTypeEnum getImUserType(String type) { + System.out.println( type); + for (ImUserTypeEnum value : values()) { + if (value.name().equals(type)) { + return value; + } + } + return null; + } + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/MessageTypeEnum.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/MessageTypeEnum.java new file mode 100644 index 0000000..8db3449 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/MessageTypeEnum.java @@ -0,0 +1,38 @@ +package com.sonic.pigeon.lib.enums; + +import lombok.Getter; + +@Getter +public enum MessageTypeEnum { + + TEXT(0, "文本消息"), + IMAGE(1, "图片消息"), + VOICE(2, "语音消息"), + VIDEO(3, "视频消息"), + GEO(4, "地理位置消息"), + FILE(6, "文件消息"), + NOTIFY(10, "提示消息"), + CUSTOM(100, "自定义消息"), + + ; + + + private int code; + + private String desc; + + MessageTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static MessageTypeEnum getByCode(int code) { + for (MessageTypeEnum value : values()) { + if (value.code == code) { + return value; + } + } + return null; + } + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/StationMessageTypeEnum.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/StationMessageTypeEnum.java new file mode 100644 index 0000000..f66eba6 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/enums/StationMessageTypeEnum.java @@ -0,0 +1,68 @@ +package com.sonic.pigeon.lib.enums; + +/** + * @description 消息类型 + * @author: zzhan + * @create: 2018-06-11 10:56 + **/ +public enum StationMessageTypeEnum { + + //注册后欢迎通知 + REGISTER_WELCOME(100, "注册后欢迎通知","About CrushLevel AI","Experience a love story with Crushlevel AI: From \"hello\" to \"I do\", every conversation is up the ante. At Crushlevel AI, every conversation is writing your love epic - from the jerky \"hello\" to the trembling \"marry me. Those love words that dare not be said in reality, the response that cannot be waited, the temptation that is afraid of getting hurt, finally find a place to rest." ), + //虚拟角色主动问候 + AI_GREETING(101, "虚拟角色主动问候","",""), + //会员续订成功 + MEMBER_RENEW_SUCCESS(102, "会员续订成功","Membership renewal was successful","Your heart membership has been automatically renewed successfully, and the membership benefits have been extended to %s."), + //会员续订失败 + MEMBER_RENEW_FAIL(103, "会员续订失败","Membership renewal failed","Your heart membership has been automatically renewed and failed, and the membership rights have been terminated."), + //角色被送礼 + AI_GIFT(104, "角色被送礼","New gift.","Your character \"%s\" has received a new gift \"%s *%s\", earning %s Crush Coin."), + //角色被解锁图片 + AI_IMG_UNLOCK(105, "角色被解锁图片","Image is unlocked","Your character \"%s\" has new pictures unlocked, earning %s Crush Coins."), + //心动等级降级 + HEARTBEAT_LEVEL_DOWN(106, "心动等级降级","Cardiac grade reduction","Due to a long absence of contact, your heart level with avatar \"%s\" has been reduced to %s"), + + ; + + private final Integer index; + private final String name; + private final String title; + private final String content; + + StationMessageTypeEnum(Integer index, String name, String title, String content) { + this.index = index; + this.name = name; + this.title = title; + this.content = content; + } + + public Integer getIndex() { + return index; + } + + public String getName() { + return name; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + + public static String getName(Integer index) { + if(index == null) { + return null; + } + for (StationMessageTypeEnum c : StationMessageTypeEnum.values()) { + if (c.getIndex().equals(index)) { + return c.getName(); + } + } + return "未知"; + } + + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/AiImInfoExtensionInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/AiImInfoExtensionInput.java new file mode 100644 index 0000000..f737e7c --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/AiImInfoExtensionInput.java @@ -0,0 +1,20 @@ +package com.sonic.pigeon.lib.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Im的扩展信息 + */ + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class AiImInfoExtensionInput { + + private String nickname; + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/ConversationExtensionInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/ConversationExtensionInput.java new file mode 100644 index 0000000..9b3ab27 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/ConversationExtensionInput.java @@ -0,0 +1,29 @@ +package com.sonic.pigeon.lib.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ConversationExtensionInput { + + @ApiModelProperty("心动分值") + private BigDecimal heartbeatVal; + + @ApiModelProperty("心动等级") + private String heartbeatLevel; + + @ApiModelProperty("关系展示开关 0:关闭 1:展示") + private Boolean isShow; + + @ApiModelProperty("操作类型:空或0 无操作、1 赞、2 踩") + private Integer optType; + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/CreateConversationInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/CreateConversationInput.java new file mode 100644 index 0000000..512deb4 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/CreateConversationInput.java @@ -0,0 +1,23 @@ +package com.sonic.pigeon.lib.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class CreateConversationInput { + + private Long fromUserId; + + private Long toUserId; + + /** + * 扩展字段,使用 ConversationExtensionInput 类的定义 + */ + private String extension; + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/CreateImUserInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/CreateImUserInput.java new file mode 100644 index 0000000..5dcbd55 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/CreateImUserInput.java @@ -0,0 +1,37 @@ +package com.sonic.pigeon.lib.input; + +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class CreateImUserInput { + + private Long userId; + + private ImUserTypeEnum imUserType; + + private String nickname; + + private String headImage; + + /** + * 用户生日,例如 "xxxx-xx-xx"。 + */ + private String birthday; + + /** + * 用户性别,0-未知,1-男,2-女。 + */ + private Integer gender; + + /** + * 扩展字段 请使用AiImInfoExtensionInput对象填充 + */ + private Object extension; +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/EditImUserInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/EditImUserInput.java new file mode 100644 index 0000000..77d3a97 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/EditImUserInput.java @@ -0,0 +1,37 @@ +package com.sonic.pigeon.lib.input; + +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class EditImUserInput { + + Long userId; + + ImUserTypeEnum imUserType; + + String nickname; + + String headImage; + + /** + * 用户生日,例如 "xxxx-xx-xx"。 + */ + private String birthday; + + /** + * 用户性别,0-未知,1-男,2-女。 + */ + private Integer gender; + + /** + * 扩展字段 请使用AiImInfoExtensionInput对象填充 + */ + private Object extension; +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/HistoryMessageInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/HistoryMessageInput.java new file mode 100644 index 0000000..8fa71a4 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/HistoryMessageInput.java @@ -0,0 +1,32 @@ +package com.sonic.pigeon.lib.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class HistoryMessageInput { + + @ApiModelProperty("发送人用户ID") + private Long fromUserId; + + @ApiModelProperty("接收人用户ID") + private Long toUserId; + + @ApiModelProperty("开始时间") + private Long beginTime; + + @ApiModelProperty("结束时间") + private Long endTime; + + @ApiModelProperty("每页返回的消息数上限,最大为 100") + private Integer limit; + + @ApiModelProperty("是否按时间正序,true:按时间正序;false:按时间倒序") + private Boolean descending; +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/SendAiCustomerMessageInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/SendAiCustomerMessageInput.java new file mode 100644 index 0000000..759b4a1 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/SendAiCustomerMessageInput.java @@ -0,0 +1,27 @@ +package com.sonic.pigeon.lib.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class SendAiCustomerMessageInput { + + private Long fromUserId; + + private Long toUserId; + + @ApiModelProperty("消息的文本内容") + private String content; + + @ApiModelProperty("自定义消息内容") + private String attachment; + + @ApiModelProperty("服务端扩展字段") + private String extension; +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/SendAiTextMessageInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/SendAiTextMessageInput.java new file mode 100644 index 0000000..0cc69e0 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/SendAiTextMessageInput.java @@ -0,0 +1,24 @@ +package com.sonic.pigeon.lib.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class SendAiTextMessageInput { + + private Long fromUserId; + + private Long toUserId; + + private String content; + + /** + * 服务端扩展字段 + */ + private String extension; +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/SendMessageInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/SendMessageInput.java new file mode 100644 index 0000000..c2af53d --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/SendMessageInput.java @@ -0,0 +1,136 @@ +package com.sonic.pigeon.lib.input; + +import io.swagger.annotations.ApiModelProperty; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author zzhan + * @Date 2020/10/15 20:28 + * @Version 1.0 + */ +public class SendMessageInput { + + @ApiModelProperty(value = "发送人ID") + private Long sendUserId; + + @ApiModelProperty(value = "用户ID(接收人)") + private Long userId; + + @ApiModelProperty(value = "消息类型(对应 StationMessageTypeEnum 枚举 中的index字段值)") + private Integer type; + + @ApiModelProperty(value = "导致消息发送的业务ID") + private String bizId; + + @ApiModelProperty(value = "消息标题") + private String title; + + @ApiModelProperty(value = "消息内容") + private String content; + + @ApiModelProperty(value = "消息扩展内容") + private String extras; + + @ApiModelProperty(value = "消息替换内容,例('nickname','email')") + private List titleReplaceList = new ArrayList<>(); + + @ApiModelProperty(value = "消息替换内容,例('nickname','email')") + private List contentReplaceList = new ArrayList<>(); + + + public SendMessageInput() { + } + + public SendMessageInput(Long sendUserId, Long userId, Integer type, String title, String content) { + this.sendUserId = sendUserId; + this.userId = userId; + this.type = type; + this.title = title; + this.content = content; + } + + public SendMessageInput(Long sendUserId, Long userId, Integer type, String bizId, String title, String content) { + this.sendUserId = sendUserId; + this.userId = userId; + this.type = type; + this.bizId = bizId; + this.title = title; + this.content = content; + } + + public List getTitleReplaceList() { + return titleReplaceList; + } + + public void setTitleReplaceList(List titleReplaceList) { + this.titleReplaceList = titleReplaceList; + } + + public List getContentReplaceList() { + return contentReplaceList; + } + + public void setContentReplaceList(List contentReplaceList) { + this.contentReplaceList = contentReplaceList; + } + + public Long getSendUserId() { + return sendUserId; + } + + public void setSendUserId(Long sendUserId) { + this.sendUserId = sendUserId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Integer getType() { + return type; + } + + public void setType(Integer type) { + this.type = type; + } + + public String getBizId() { + return bizId; + } + + public void setBizId(String bizId) { + this.bizId = bizId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getExtras() { + return extras; + } + + public void setExtras(String extras) { + this.extras = extras; + } + + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/UpdateAiSendCustomImageMessageInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/UpdateAiSendCustomImageMessageInput.java new file mode 100644 index 0000000..6315529 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/UpdateAiSendCustomImageMessageInput.java @@ -0,0 +1,24 @@ +package com.sonic.pigeon.lib.input; + +import com.sonic.pigeon.lib.bo.AttachBo; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UpdateAiSendCustomImageMessageInput { + + @ApiModelProperty("发送人用户ID") + private Long aiId; + @ApiModelProperty("接收人用户ID") + private Long userId; + @ApiModelProperty("消息ID") + private Long messageServerId; + @ApiModelProperty("扩展字段") + private AttachBo attachment; +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/UpdateConversationInput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/UpdateConversationInput.java new file mode 100644 index 0000000..edd1f99 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/input/UpdateConversationInput.java @@ -0,0 +1,23 @@ +package com.sonic.pigeon.lib.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UpdateConversationInput { + + private Long fromUserId; + + private Long toUserId; + + /** + * 扩展字段,使用 ConversationExtensionInput 类的定义 + */ + private String extension; + +} diff --git a/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/output/HistoryMessageOutput.java b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/output/HistoryMessageOutput.java new file mode 100644 index 0000000..21b7c68 --- /dev/null +++ b/sonic-pigeon/lib/src/main/java/com/sonic/pigeon/lib/output/HistoryMessageOutput.java @@ -0,0 +1,27 @@ +package com.sonic.pigeon.lib.output; + +import com.sonic.pigeon.lib.enums.MessageTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class HistoryMessageOutput { + + private Long fromUserId; + + private Long toUserId; + + private String content; + + private MessageTypeEnum messageType; + + private Object attachment; + + private String extension; + + private Long createTime; + +} diff --git a/sonic-pigeon/pom.xml b/sonic-pigeon/pom.xml new file mode 100644 index 0000000..108c59a --- /dev/null +++ b/sonic-pigeon/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + com.sonic + common-parent-pom + 1.0 + + + sonic-pigeon + + com.sonic.pigeon + pom + 1.0-SNAPSHOT + + + + 1.0.6 + 1.0 + + + + + + com.sonic + common-lib + ${common-lib.version} + + + com.sonic + dao-support-lib + ${dao-support-lib.version} + + + + + + + org.projectlombok + lombok + + + + + common + server + lib + + + + + + + + + + + + + + diff --git a/sonic-pigeon/server/pom.xml b/sonic-pigeon/server/pom.xml new file mode 100644 index 0000000..70c0d1d --- /dev/null +++ b/sonic-pigeon/server/pom.xml @@ -0,0 +1,140 @@ + + + + sonic-pigeon + com.sonic.pigeon + 1.0 + + 4.0.0 + + sonic-pigeon-server + jar + + + + com.sonic.pigeon + sonic-pigeon-common + 1.0-SNAPSHOT + + + + com.sonic.sdk + sonic-common-api + 1.0.1-SNAPSHOT + + + + com.sonic + dao-support-lib + 1.0 + + + + com.sonic.pigeon + sonic-pigeon-lib + 1.0-SNAPSHOT + + + + com.sonic.lion + sonic-lion-lib + 1.0-SNAPSHOT + + + + com.sonic.frog + sonic-frog-lib + 1.1-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + it.ozimov + embedded-redis + + + + + org.redisson + redisson-spring-boot-starter + 3.13.1 + + + + + org.springframework.amqp + spring-amqp + + + org.springframework.amqp + spring-rabbit + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + + + com.h2database + h2 + + + + com.github.ben-manes.caffeine + caffeine + + + com.sonic.pigeon + sonic-pigeon-lib + 1.1-SNAPSHOT + compile + + + + com.netease.nim + yunxin-server-sdk + 1.1.1 + + + com.sonic.pigeon + sonic-pigeon-lib + 1.1-SNAPSHOT + compile + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pl.project13.maven + git-commit-id-plugin + + + false + false + + + + + diff --git a/sonic-pigeon/server/src/main/java/com/baomidou/mybatisplus/core/toolkit/Sequence.java b/sonic-pigeon/server/src/main/java/com/baomidou/mybatisplus/core/toolkit/Sequence.java new file mode 100644 index 0000000..d41c757 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/baomidou/mybatisplus/core/toolkit/Sequence.java @@ -0,0 +1,175 @@ +package com.baomidou.mybatisplus.core.toolkit; + +import lombok.extern.slf4j.Slf4j; + +import java.net.InetAddress; + +/** + * 原始链接 https://segmentfault.com/a/1190000020835840 + * 雪花算法分布式唯一ID生成器
+ * 每个机器号最高支持每秒‭65535个序列, 当秒序列不足时启用备份机器号, 若备份机器也不足时借用备份机器下一秒可用序列
+ * 53 bits 趋势自增ID结构如下: + *

+ * |00000000|00011111|11111111|11111111|11111111|11111111|11111111|11111111| + * |-----------|##########32bit 秒级时间戳##########|-----|-----------------| + * |--------------------------------------5bit机器位|xxxxx|-----------------| + * |-----------------------------------------16bit自增序列|xxxxxxxx|xxxxxxxx| + * + * @author: + * @date: 2021-12-30 + **/ +@Slf4j +public class Sequence { + /** + * 初始偏移时间戳 + */ + private final long OFFSET = 1546300800L; + + /** + * 机器id (0~15 保留 16~31作为备份机器) + */ + private long WORKER_ID; + /** + * 机器id所占位数 (5bit, 支持最大机器数 2^5 = 32) + */ + private final long WORKER_ID_BITS = 5L; + /** + * 自增序列所占位数 (16bit, 支持最大每秒生成 2^16 = ‭65536‬) + */ + private final long SEQUENCE_ID_BITS = 16L; + /** + * 机器id偏移位数 + */ + private final long WORKER_SHIFT_BITS = SEQUENCE_ID_BITS; + /** + * 自增序列偏移位数 + */ + private final long OFFSET_SHIFT_BITS = SEQUENCE_ID_BITS + WORKER_ID_BITS; + /** + * 机器标识最大值 (2^5 / 2 - 1 = 15) + */ + private final long WORKER_ID_MAX = ((1 << WORKER_ID_BITS) - 1) >> 1; + /** + * 备份机器ID开始位置 (2^5 / 2 = 16) + */ + private final long BACK_WORKER_ID_BEGIN = (1 << WORKER_ID_BITS) >> 1; + /** + * 自增序列最大值 (2^16 - 1 = ‭65535) + */ + private final long SEQUENCE_MAX = (1 << SEQUENCE_ID_BITS) - 1; + /** + * 发生时间回拨时容忍的最大回拨时间 (秒) + */ + private final long BACK_TIME_MAX = 1L; + + /** + * 上次生成ID的时间戳 (秒) + */ + private long lastTimestamp = 0L; + /** + * 当前秒内序列 (2^16) + */ + private long sequence = 0L; + /** + * 备份机器上次生成ID的时间戳 (秒) + */ + private long lastTimestampBak = 0L; + /** + * 备份机器当前秒内序列 (2^16) + */ + private long sequenceBak = 0L; + + public static int WORK_ID = 0; + + /** + * 使用ip第三位作为工作线程ID + */ + static { + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + String last = inetAddress.getHostAddress().split("\\.")[3]; + WORK_ID = Integer.valueOf(last) % 15; + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 私有构造函数禁止外部访问 + */ + public Sequence() { + WORKER_ID = WORK_ID; + } + + public Sequence(long workerId, long datacenterId) { + WORKER_ID = WORK_ID; + } + + /** + * 获取自增序列 + * + * @return long + */ + public long nextId() { + return nextId(SystemClock.now() / 1000); + } + + /** + * 主机器自增序列 + * + * @param timestamp 当前Unix时间戳 + * @return long + */ + private synchronized long nextId(long timestamp) { + // 时钟回拨检查 + if (timestamp < lastTimestamp) { + // 发生时钟回拨 + log.warn("时钟回拨, 启用备份机器ID: now: [{}] last: [{}]", timestamp, lastTimestamp); + return nextIdBackup(timestamp); + } + + // 开始下一秒 + if (timestamp != lastTimestamp) { + lastTimestamp = timestamp; + sequence = 0L; + } + if (0L == (++sequence & SEQUENCE_MAX)) { + // 秒内序列用尽 + log.warn("秒内[{}]序列用尽, 启用备份机器ID序列", timestamp); + sequence--; + return nextIdBackup(timestamp); + } + + return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | (WORKER_ID << WORKER_SHIFT_BITS) | sequence; + } + + /** + * 备份机器自增序列 + * + * @param timestamp timestamp 当前Unix时间戳 + * @return long + */ + private long nextIdBackup(long timestamp) { + if (timestamp < lastTimestampBak) { + if (lastTimestampBak - SystemClock.now() / 1000 <= BACK_TIME_MAX) { + timestamp = lastTimestampBak; + } else { + throw new RuntimeException(String.format("时钟回拨: now: [%d] last: [%d]", timestamp, lastTimestampBak)); + } + } + + if (timestamp != lastTimestampBak) { + lastTimestampBak = timestamp; + sequenceBak = 0L; + } + + if (0L == (++sequenceBak & SEQUENCE_MAX)) { + // 秒内序列用尽 + log.warn("秒内[{}]序列用尽, 备份机器ID借取下一秒序列", timestamp); + return nextIdBackup(timestamp + 1); + } + + return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | ((WORKER_ID ^ BACK_WORKER_ID_BEGIN) << WORKER_SHIFT_BITS) | sequenceBak; + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/MainApplication.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/MainApplication.java new file mode 100644 index 0000000..0c573f4 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/MainApplication.java @@ -0,0 +1,22 @@ +package com.sonic.pigeon; + +import com.sonic.sdk.api.annotation.EnableDecrypt; +import com.sonic.sdk.api.annotation.EnableGatWayAuthScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * @author coder + */ +@EnableSwagger2 +@ComponentScan(value = {"com.sonic"}) +@SpringBootApplication +@EnableGatWayAuthScan(basePackages = "com.sonic.pigeon.controller") +@EnableDecrypt +public class MainApplication { + public static void main(String[] args) { + SpringApplication.run(MainApplication.class, args); + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/Config.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/Config.java new file mode 100644 index 0000000..463d53a --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/Config.java @@ -0,0 +1,48 @@ +package com.sonic.pigeon.config; + +import com.sonic.pigeon.common.GlobalConfig; +import com.sonic.pigeon.common.MybatisPlusConfig; +import com.sonic.common.AppRuntime; +import com.sonic.common.config.DefaultWebMvcConfig; +import com.sonic.common.exception.AbstractExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; + +import java.util.function.Consumer; + +/** + * Server服务唯一的配置入口 + * TODO: Import的各种Config,按需使用(例如,如果不需要mysql,kafka消息,redis等,可以去掉对应的Config) + * + * @author coder + */ +@Slf4j +@Configuration +@Import({GlobalConfig.class, DefaultWebMvcConfig.class, MybatisPlusConfig.class, RedisConfig.class, + EventConfig.class, SwaggerConfig.class}) +public class Config { + + /** + * XXX: ExceptionHandlerConfig 应该在 DefaultWebMvcConfig 前被初始. 否则 DefaultWebMvcConfig 的 exceptionHandlerHook 会生效 + */ + static class ExceptionHandlerConfig { + @Bean + @Profile({"!unittest && !local"}) + AbstractExceptionHandler.ExceptionHandlerHook exceptionHandlerHook(AppRuntime appRuntime) { + return new AbstractExceptionHandler.ExceptionHandlerHook() { + @Override + public String getAppId() { + return appRuntime.getAppId(); + } + + @Override + public Consumer getExceptionConsumer() { + return context -> log.error("未捕获内部异常, logText: {}", context.dumpRequest(), context.getException()); + } + }; + } + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/EventConfig.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/EventConfig.java new file mode 100644 index 0000000..20c76a2 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/EventConfig.java @@ -0,0 +1,188 @@ +package com.sonic.pigeon.config; + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.ImmutableMap; +import com.rabbitmq.client.Channel; +import com.sonic.common.AppRuntime; +import com.sonic.common.event.*; +import com.sonic.common.utils.LogUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.annotation.RabbitListeners; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Payload; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * @author coder + */ +@Slf4j +public class EventConfig { + + /** TODO: 定义 Event.BuildInScene */ + public final static String DEFAULT_SCENE = Event.BuildInScene.SONIC.getCode(); + /** TODO: DEFAULT_SCENE + "_" + appName */ + public final static String PIGEON = DEFAULT_SCENE + "_" + "pigeon"; + public final static String FROG = DEFAULT_SCENE + "_" + "frog"; + + @Value("${mq.exchange}") + private String mqExchange; + @Value("${mq.default.queue}") + private String defaultQueue; + @Value("${mq.default.routing-key}") + private String defaultRoutingKey; + + @Value("${mq.aiChat.queue}") + private String aiChatQueue; + @Value("${mq.aiChat.routing-key}") + private String aiChatRoutingKey; + + @Value("${mq.aiChatToFrog.queue}") + private String aiChatToFrogQueue; + @Value("${mq.aiChatToFrog.routing-key}") + private String aiChatToFrogRoutingKey; + + @Value("${mq.user-balance-insufficient-checkout.queue}") + private String userBalanceInsufficientCheckoutQueue; + @Value("${mq.user-balance-insufficient-checkout.routing-key}") + private String userBalanceInsufficientCheckoutRoutingKey; + + + @Configuration + @Profile("!unittest") + static class Listener { + @Autowired + EventConsumer eventConsumer; + + @RabbitListeners({ + @RabbitListener(queues = {"${mq.default.queue}"}, concurrency = "2"), + }) + public void process(@Payload Message message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) { + try { + LogUtils.setTraceId(); + byte[] msgBody = message.getBody(); + String contentEncoding = message.getMessageProperties().getContentEncoding(); + String bodyStr = new String(msgBody, Charset.forName(ObjectUtils.defaultIfNull(contentEncoding, "UTF-8"))); + MessageProperties pro = message.getMessageProperties(); + if (log.isDebugEnabled()) { + log.info("receive msg routingKey:{}->consumerQueue:{}->redelivered:{}->body:{}", + pro.getReceivedRoutingKey(), pro.getConsumerQueue(), pro.getRedelivered(), bodyStr); + } + eventConsumer.onEvent(bodyStr, ImmutableMap.of("record", JSON.toJSONString(message))); + + //消息确认,multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息 + channel.basicAck(deliveryTag, false); + } catch (Exception e) { + log.error("===> mq handler error, message: {}", JSON.toJSONString(message), e); + //消息拒绝,//requeue=true,表示将消息重新放入到队列中,false:表示直接从队列中删除,此时和basicAck(long deliveryTag, false)的效果一样 + try { + channel.basicReject(deliveryTag,true); + } catch (IOException ioException) { + log.error("channel.basicReject error. message: {}, deliveryTag: {}", JSON.toJSONString(message), deliveryTag, ioException); + } + } finally { + LogUtils.removeTraceId(); + } + } + + } + + @Bean + EventProducer eventProducer(AppRuntime appRuntime, RabbitTemplate rabbitTemplate, TaskExecutor taskExecutor) { + BiConsumer callback = (event, meta) -> {}; + return new RabbitmqEventProducer(rabbitTemplate, PIGEON, DEFAULT_SCENE, appRuntime.getAppId(), + RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, defaultRoutingKey), taskExecutor, callback); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta aiChatMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, aiChatRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta aiChatToFrogMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, aiChatToFrogRoutingKey); + } + + @Bean + RabbitmqEventProducer.RabbitmqMessageMeta userBalanceInsufficientCheckoutMeta() { + return RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, userBalanceInsufficientCheckoutRoutingKey); + } + + @Bean + EventConsumer eventConsumer(AppRuntime appRuntime, EventHandlerRepository eventHandlerRepository) { + Consumer callback = (eventWrapper) -> {}; + return new DefaultEventConsumer(appRuntime.getAppName(), eventHandlerRepository, callback); + } + + @Bean + EventHandlerRepository eventHandlerRepository() { + return new EventHandlerRepository((ex, logText) -> log.error("MQ,handle error {}", logText, ex)); + } + + @Bean + public DirectExchange messageServerExchange(){ + return new DirectExchange(mqExchange); + } + + @Bean + public Binding bindingDefaultQueueExchange(DirectExchange exchange, Queue defaultQueue) { + return bindingExchange(exchange, defaultQueue, defaultRoutingKey); + } + + @Bean + public Binding bindingAiChatQueueExchange(DirectExchange exchange) { + return bindingExchange(exchange, aiChatQueue(), aiChatRoutingKey); + } + + @Bean + public Binding bindingAiChatToFrogQueueExchange(DirectExchange exchange) { + return bindingExchange(exchange, aiChatToFrogQueue(), aiChatToFrogRoutingKey); + } + + @Bean + public Binding bindingUserBalanceInsufficientCheckoutQueueExchange(DirectExchange exchange, Queue userBalanceInsufficientCheckoutQueue) { + return bindingExchange(exchange, userBalanceInsufficientCheckoutQueue, userBalanceInsufficientCheckoutRoutingKey); + } + + + @Bean + public Queue defaultQueue() { + return new Queue(defaultQueue,true); + } + + @Bean + public Queue aiChatQueue() { + return new Queue(aiChatQueue,true); + } + + @Bean + public Queue aiChatToFrogQueue() { + return new Queue(aiChatToFrogQueue,true); + } + + @Bean + public Queue userBalanceInsufficientCheckoutQueue() { + return new Queue(userBalanceInsufficientCheckoutQueue,true); + } + + public Binding bindingExchange(DirectExchange exchange, Queue queueMessage, String routingKey) { + return BindingBuilder.bind(queueMessage).to(exchange).with(routingKey); + } + +} + diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/ImConfig.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/ImConfig.java new file mode 100644 index 0000000..7f0163b --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/ImConfig.java @@ -0,0 +1,31 @@ +package com.sonic.pigeon.config; + +import com.netease.nim.server.sdk.core.BizName; +import com.netease.nim.server.sdk.core.YunxinApiHttpClient; +import com.netease.nim.server.sdk.core.endpoint.Region; +import com.netease.nim.server.sdk.im.v2.YunxinV2ApiServices; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ImConfig { + + @Value("${im.appKey:xxx}") + private String appKey; + @Value("${im.appSecret:xxx}") + private String appSecret; + @Value("${im.timeoutMillis:5000}") + private int timeoutMillis; + + @Bean + public YunxinV2ApiServices yunxinV2ApiServices() { + YunxinApiHttpClient client = new YunxinApiHttpClient.Builder(BizName.IM, appKey, appSecret) + .region(Region.SG) + .timeoutMillis(timeoutMillis) + .build(); + // services + return new YunxinV2ApiServices(client); + } + +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/RedisConfig.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/RedisConfig.java new file mode 100644 index 0000000..ca147b9 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/RedisConfig.java @@ -0,0 +1,39 @@ +package com.sonic.pigeon.config; + +import com.sonic.common.utils.RedisLock; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * 有关Redis的配置 + * + * TODO: 如果不需要Redis, 可以删除该文件并且在{@link Config}类@Import的注解中移除对RedisConfig的引用 + */ +@Slf4j +public class RedisConfig { + + /** + * redisWrapper用于分布式锁RedisLock + * + * @param redisTemplate + * @return + */ + @Bean + RedisLock.RedisWrapper redisWrapper(RedisTemplate redisTemplate) { + return new RedisLock.RedisWrapper() { + @Override + public void delete(String key) { + redisTemplate.delete(key); + } + + @Override + public boolean lock(String key, String value, long expireMills) { + return redisTemplate.opsForValue().setIfAbsent(key, value, expireMills, TimeUnit.MILLISECONDS); + } + }; + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/RedissonConfig.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/RedissonConfig.java new file mode 100644 index 0000000..b897deb --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/RedissonConfig.java @@ -0,0 +1,52 @@ +package com.sonic.pigeon.config; + +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + + /** + * 配置 RedissonClient,支持单机和集群模式 + */ + @Bean + public RedissonClient redissonClient(RedisProperties redisProperties) { + Config config = new Config(); + + // 设置 Netty 线程数,使用默认值 0(Redisson 默认使用 CPU 核心数的两倍) + config.setNettyThreads(0); + + // 判断是否配置了集群节点 + if (redisProperties.getCluster() != null && redisProperties.getCluster().getNodes() != null + && !redisProperties.getCluster().getNodes().isEmpty()) { + // 集群模式配置 + config.useClusterServers() + .setScanInterval(2000) // 集群状态扫描间隔,单位毫秒 + .addNodeAddress(redisProperties.getCluster().getNodes().stream() + .map(node -> "redis://" + node) + .toArray(String[]::new)) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null); + } else { + // 单机模式配置 + config.useSingleServer() + .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort()) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null) + .setDatabase(redisProperties.getDatabase()); + } + + // 如果启用了 SSL,设置 SSL 相关配置 + if (redisProperties.isSsl()) { + config.useSingleServer().setSslEnableEndpointIdentification(false); // 禁用主机名验证 + // 集群模式下,Redisson 会自动应用 SSL 配置 + } + + return Redisson.create(config); + } +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/RestTemplateConfig.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/RestTemplateConfig.java new file mode 100644 index 0000000..5793363 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/RestTemplateConfig.java @@ -0,0 +1,24 @@ +package com.sonic.pigeon.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(ClientHttpRequestFactory factory){ + return new RestTemplate(factory); + } + + @Bean + public ClientHttpRequestFactory simpleClientHttpRequestFactory(){ + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setReadTimeout(5000);//单位为ms + factory.setConnectTimeout(5000);//单位为ms + return factory; + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/ResultCode.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/ResultCode.java new file mode 100644 index 0000000..976ef9c --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/ResultCode.java @@ -0,0 +1,43 @@ +package com.sonic.pigeon.config; + +import com.sonic.common.rpc.ApiResultCode; +import lombok.Getter; + +/** + * @author coder + */ +@Getter +public enum ResultCode implements ApiResultCode { + + /** + * TODO: 可以在此处扩展服务自身需要用到的错误码信息 + */ + BIZ_ERROR1("0000", "业务异常1"), + DEMO_CREATED_FAIL("0001", "新增Demo实体失败"); + + private final String errorCode; + private final String errorMsg; + + ResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/SsoConfig.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/SsoConfig.java new file mode 100644 index 0000000..3eb85c2 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/SsoConfig.java @@ -0,0 +1,16 @@ +package com.sonic.pigeon.config; + +import com.sonic.common.auth.GateWaySessionInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SsoConfig { + + + @Bean + public GateWaySessionInterceptor gateWaySessionInterceptor() { + return new GateWaySessionInterceptor(); + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/SwaggerConfig.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/SwaggerConfig.java new file mode 100644 index 0000000..a22f1bf --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/SwaggerConfig.java @@ -0,0 +1,110 @@ +package com.sonic.pigeon.config; + + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import springfox.documentation.RequestHandler; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; + +/** + * swagger配置 + * @author code + */ +@Slf4j +@EnableSwagger2 +public class SwaggerConfig { + + private static final String SPLIT = ","; + + @Value("${swagger.enabled:false}") + private Boolean swaggerEnabled; + + @Value("${swagger.base.package}") + private String basePackageValue; + + private final String DEFAULT_BASE_PACKAGE = "com.sonic.bear.controller"; + + + public static Predicate basePackage(final String basePackage) { + return input -> declaringClass(input).transform(handlerPackage(basePackage)).or(true); + } + + private static Optional> declaringClass(RequestHandler input) { + return Optional.fromNullable(input.declaringClass()); + } + + private static Function, Boolean> handlerPackage(final String basePackage) { + return input -> { + // 循环判断匹配 + for (String strPackage : basePackage.split(SPLIT)) { + boolean isMatch = input.getPackage().getName().startsWith(strPackage); + if (isMatch) { + return true; + } + } + return false; + }; + } + + @Bean + public Docket createRestApi() { + basePackageValue = null == basePackageValue || ("").equals(basePackageValue) ? DEFAULT_BASE_PACKAGE : basePackageValue; + log.info("===> swagger base package : ", basePackageValue); + + //在配置好的配置类中增加此段代码即可 + ParameterBuilder ticketPar = new ParameterBuilder(); + List pars = new ArrayList(); + ticketPar.name("_tk_") + //name表示名称,description表示描述 + .description("token") + .modelRef(new ModelRef("string")) + .parameterType("header") + .required(false) + .defaultValue("") + .build();//required表示是否必填,defaultvalue表示默认值 + + //添加完此处一定要把下边的带***的也加上否则不生效 + pars.add(ticketPar.build()); + + + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .enable(swaggerEnabled) + .select() + .apis(basePackage(basePackageValue)) + .paths(PathSelectors.any()) + .build() + //************把消息头添加 + .globalOperationParameters(pars); + } + + /** + * TODO: 更改文案配置 + * @return + */ + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("sonic-bear") + .description("sonic-bear API") + .version("1.0") + .contact(new Contact("sonic-bear", "", "admin.sonic-bear.com")) + .build(); + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/limit/RequestLimit.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/limit/RequestLimit.java new file mode 100644 index 0000000..e4c2cae --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/limit/RequestLimit.java @@ -0,0 +1,46 @@ +package com.sonic.pigeon.config.limit; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.*; + +/** + * @description: 限流注解 + * @author: zhenqiang.zhan + * @create: 2020-03-09 23:13 + **/ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +@Order(Ordered.HIGHEST_PRECEDENCE) +public @interface RequestLimit { + + /** + * 允许访问的最大次数 + */ + int count() default Integer.MAX_VALUE; + + /** + * 已登录用户允许访问的最大次数(备注:如果不配置值的话默认都走IP的限制) + * @return + */ + int loginCount() default Integer.MAX_VALUE; + + /** + * 时间段,单位为毫秒,默认值一分钟 + */ + long time() default 60000; + + /** + * 未登录请求用户达到限流时的提示 异常码 默认值为:1001009 / 如果想要未登录用户跳转到登录页面则返回异常码为 noLoginErrorCode = "10050001" + * @return + */ + String noLoginErrorCode() default "1001009"; + + /** + * message 提示文案 + * @return + */ + String message() default "Sorry to detect your abnormal access, please try again later"; +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/limit/RequestLimitContract.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/limit/RequestLimitContract.java new file mode 100644 index 0000000..3b56e10 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/config/limit/RequestLimitContract.java @@ -0,0 +1,110 @@ +package com.sonic.pigeon.config.limit; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.enums.AppEnv; +import com.sonic.common.utils.IpAddressUtils; +import com.sonic.pigeon.enums.BizResultCode; +import com.sonic.pigeon.enums.BusinessException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.concurrent.TimeUnit; + +/** + * 限流 + */ +@Order(99) +@Slf4j +@Aspect +@Component +public class RequestLimitContract { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Value("${spring.profiles.active}") + private String runMode; + + /** + * redis key不存在的过期时间值 + */ + private Integer NULL_KEY_EXP_TIME_VALUE = -2; + + /** + * redis key永不过期的过期时间值 + */ + private Integer NEVER_EXPIRES_VALUE = -1; + + /** + * 默认的异常码 + */ + private static final String DEFAULT_ERROR_CODE = "1001009"; + + @Before("execution(public * com.sonic.*.controller..*.*(..)) && @annotation(limit)") + public void requestLimit(final JoinPoint joinPoint, RequestLimit limit) { + //dev环境直接放行不做拦截 + if (StringUtils.isNotBlank(runMode) && AppEnv.dev.name().equals(runMode)) { + return; + } + Object[] args = joinPoint.getArgs(); + HttpServletRequest request = null; + Session session = null; + for (int i = 0; i < args.length; i++) { + //解析方法的HttpServletRequest入参对象 + if (args[i] instanceof HttpServletRequest) { + request = (HttpServletRequest) args[i]; + } + //解析方法的Session入参对象、必须要配置了登录次数限制的才能进行解析 + if (args[i] instanceof Session && limit.loginCount() != Integer.MAX_VALUE) { + session = (Session) args[i]; + } + } + //请求对象为空,则抛出异常 + BizResultCode.SYS_PARAMETERS_VALIDATE_EXCEPTION.check(request == null); + int num = 0; + String ipOrUserId = null; + Integer limitCount = null; + String url = null; + boolean loginUserBl = false; + try { + num = 0; + ipOrUserId = (session == null || session.getUserId() == null) ? IpAddressUtils.getIpAddress(request) : session.getUserId().toString(); + limitCount = (session == null || session.getUserId() == null) ? limit.count() : limit.loginCount(); + loginUserBl = (session == null || session.getUserId() == null) ? false : true; + url = request.getRequestURI(); + String key = "req:limit:".concat(url).concat(":").concat(StringUtils.isNotEmpty(ipOrUserId) ? ipOrUserId.replace(":", "-") : ipOrUserId); + //redis key eg: req:limit:/web/user-search/info-to-detail:127.0.0.1 + log.info("RequestLimitContract key:{}", key); + //处理限流为-1的情况 + Long expTime = stringRedisTemplate.getExpire(key); + if (expTime == null || expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().set(key, "1", limit.time(), TimeUnit.MILLISECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(key, 1).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis的key + stringRedisTemplate.delete(key); + } + } catch (Exception e) { + log.error("requestLimit error", e); + } + log.info("用户IP[" + ipOrUserId + "]访问地址[" + url + "]===>第[" + num + "]次"); + if (num > limitCount) { + log.info("用户IP[" + ipOrUserId + "]访问地址[" + url + "]超过了限定的次数[" + limit.count() + "]"); + //未登录的用户达到限流时如果配置了跳转登录页的errorCode的话前端会直接去跳转登录页面 + throw new BusinessException(loginUserBl ? DEFAULT_ERROR_CODE : limit.noLoginErrorCode(), limit.message()); + } + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/ImConversationApi.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/ImConversationApi.java new file mode 100644 index 0000000..4c81739 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/ImConversationApi.java @@ -0,0 +1,43 @@ +package com.sonic.pigeon.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.pigeon.lib.input.CreateConversationInput; +import com.sonic.pigeon.lib.input.CreateImUserInput; +import com.sonic.pigeon.lib.input.UpdateConversationInput; +import com.sonic.pigeon.service.ImConversationService; +import com.sonic.pigeon.service.ImUserService; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +/** + * API-会话处理 + * @author coder + */ +@Validated +@RestController +public class ImConversationApi { + + @Autowired + private ImConversationService imConversationService; + + @ApiOperation(value = "创建会话", tags = {"API-会话处理"}) + @PostMapping("/api/im-conversation/create") + public Result createImConversation(@RequestBody @Valid CreateConversationInput input) { + imConversationService.createConversation(input); + return Result.success(); + } + + @ApiOperation(value = "更新会话扩展字段", tags = {"API-会话处理"}) + @PostMapping("/api/im-conversation/update") + public Result updateImConversation(@RequestBody @Valid UpdateConversationInput input) { + imConversationService.updateConversationExtension(input); + return Result.success(); + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/ImMessageApi.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/ImMessageApi.java new file mode 100644 index 0000000..bd754c4 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/ImMessageApi.java @@ -0,0 +1,86 @@ +package com.sonic.pigeon.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.pigeon.lib.input.HistoryMessageInput; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import com.sonic.pigeon.lib.input.SendAiTextMessageInput; +import com.sonic.pigeon.lib.input.UpdateAiSendCustomImageMessageInput; +import com.sonic.pigeon.lib.output.HistoryMessageOutput; +import com.sonic.pigeon.service.ImMessageService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +/** + * API-IM消息 + * @author coder + */ +@Validated +@RestController +public class ImMessageApi { + + @Autowired + private ImMessageService imMessageService; + + @IgnoreAuth + @ApiOperation(value = "发送Im文本消息(ai给用户发)", tags = {"API-IM消息"}) + @PostMapping("/api/im-message/send-ai-to-user-text-message") + public Result sendAiToUserTextMessage(@RequestBody @Valid SendAiTextMessageInput input) { + imMessageService.sendAiToUserTextMessage(input); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "发送Im文本消息(用户给ai发)", tags = {"API-IM消息"}) + @PostMapping("/api/im-message/send-user-to-ai-text-message") + public Result sendUserToAiTextMessage(@RequestBody @Valid SendAiTextMessageInput input) { + imMessageService.sendUserToAiTextMessage(input); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "发送Im自定义消息(ai给用户发)", tags = {"API-IM消息"}) + @PostMapping("/api/im-message/send-ai-to-user-customer-message") + public Result sendAiToUserCustomerMessage(@RequestBody @Valid SendAiCustomerMessageInput input) { + imMessageService.sendAiToUserCustomerMessage(input); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "发送用户的Im自定义消息(用户给AI发)", tags = {"API-IM消息"}) + @PostMapping("/api/im-message/send-user-to-ai-customer-message") + public Result sendUserToAiCustomerMessage(@RequestBody @Valid SendAiCustomerMessageInput input) { + imMessageService.sendUserToAiCustomerMessage(input); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "获取历史消息", tags = {"API-IM消息"}) + @PostMapping("/api/im-message/get-history-message") + public Result> getUserToAiHistoryMessage(@RequestBody @Validated HistoryMessageInput input) { + return Result.success(imMessageService.getUserToAiHistoryMessage(input)); + } + + @IgnoreAuth + @ApiOperation(value = "获取历史消息", tags = {"API-IM消息"}) + @PostMapping("/api/im-message/get-ai-to-user-history-message") + public Result> getAiToUserHistoryMessage(@RequestBody @Validated HistoryMessageInput input) { + return Result.success(imMessageService.getAiToUserHistoryMessage(input)); + } + + @IgnoreAuth + @ApiOperation(value = "更新自定义图片消息", tags = {"API-IM消息"}) + @PostMapping("/api/im-message/update-ai-custom-image-message") + public Result updateAiSendCustomImageMessage(@RequestBody @Validated UpdateAiSendCustomImageMessageInput input) { + imMessageService.updateAiSendCustomImageMessage(input.getAiId(), input.getUserId(), input.getMessageServerId(), input.getAttachment()); + return Result.success(); + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/ImUserApi.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/ImUserApi.java new file mode 100644 index 0000000..e4c1b3f --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/ImUserApi.java @@ -0,0 +1,41 @@ +package com.sonic.pigeon.controller.api; + +import com.sonic.common.rpc.Result; +import com.sonic.pigeon.lib.input.CreateImUserInput; +import com.sonic.pigeon.lib.input.EditImUserInput; +import com.sonic.pigeon.service.ImUserService; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +/** + * API-IM用户处理 + * @author coder + */ +@Validated +@RestController +public class ImUserApi { + + @Autowired + private ImUserService imUserService; + + @ApiOperation(value = "创建IM账号", tags = {"API-IM用户处理"}) + @PostMapping("/api/im-user/create") + public Result createImUser(@RequestBody @Valid CreateImUserInput input) { + imUserService.createImUser(input); + return Result.success(); + } + + @ApiOperation(value = "修改IM用户基础信息", tags = {"API-IM用户处理"}) + @PostMapping("/api/im-user/edit") + public Result editImUser(@RequestBody @Valid EditImUserInput input) { + imUserService.editImUser(input); + return Result.success(); + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/MessageApi.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/MessageApi.java new file mode 100644 index 0000000..7846984 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/api/MessageApi.java @@ -0,0 +1,44 @@ +package com.sonic.pigeon.controller.api; + +import com.google.common.collect.Lists; +import com.sonic.common.rpc.Result; +import com.sonic.pigeon.domain.input.SendMessageInput; +import com.sonic.pigeon.service.MessageService; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * API-消息通知 + * @author mzc + */ +@Validated +@RestController +public class MessageApi { + + @Autowired + private MessageService messageService; + + @IgnoreAuth + @ApiOperation(value = "发送IM消息且不进行消息推送", tags = {"API-站内消息"}) + @RequestMapping(value = "api/message/send-message", produces = {"application/json"}, method = RequestMethod.POST) + public Result sendMessage(@Validated @RequestBody SendMessageInput input) throws Exception { + messageService.sendMessage(Lists.newArrayList(input)); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "发送IM消息且不进行消息推送", tags = {"API-站内消息"}) + @RequestMapping(value = "api/message/batch-send-message", produces = {"application/json"}, method = RequestMethod.POST) + public Result sendMessage(@Validated @RequestBody List req) throws Exception { + messageService.sendMessage(req); + return Result.success(); + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/mock/MockController.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/mock/MockController.java new file mode 100644 index 0000000..a8d47b4 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/mock/MockController.java @@ -0,0 +1,105 @@ +package com.sonic.pigeon.controller.mock; + +import com.netease.nim.server.sdk.im.v2.users.response.GetUserResponseV2; +import com.sonic.common.rpc.Result; +import com.sonic.pigeon.domain.input.FeedbackInput; +import com.sonic.pigeon.lib.input.*; +import com.sonic.pigeon.lib.output.HistoryMessageOutput; +import com.sonic.pigeon.service.*; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +/** + * Mock测试 + */ +@Slf4j +@RestController +public class MockController { + + @Autowired + private ImMessageService imMessageService; + @Autowired + private ImUserService imUserService; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private ImConversationService imConversationService; + @Autowired + private AiChatFeedbackService aiChatFeedbackService; + + @IgnoreAuth + @ApiOperation(value = "发送Im消息v1", tags = {"Mock-IM消息"}) + @PostMapping("/mock/im-message/send-ai-message-text") + public Result sendAiTextMessage(@RequestBody @Valid SendAiTextMessageInput input) { + imMessageService.sendUserTextMessage(input); + //发送mq消息 + commonSendMqService.sendAiChatMqV1(input.getFromUserId(), input.getToUserId(), input.getContent()); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "发送Im消息v2", tags = {"Mock-IM消息"}) + @PostMapping("/mock/im-message/send-ai-message-customer") + public Result sendAiCustomerMessage(@RequestBody @Valid SendAiTextMessageInput input) { + imMessageService.sendUserTextMessage(input); + //发送mq消息 + commonSendMqService.sendAiChatMqV2(input.getFromUserId(), input.getToUserId(), input.getContent()); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "创建Im用户", tags = {"Mock-IM消息"}) + @PostMapping("/mock/im-user/create") + public Result createImUser(@RequestBody @Valid CreateImUserInput input) { + imUserService.createImUser(input); + return Result.success(imUserService.getImUser(input.getUserId(), input.getImUserType())); + } + + @IgnoreAuth + @ApiOperation(value = "获取Im用户", tags = {"Mock-IM消息"}) + @PostMapping("/mock/im-user/get") + public Result getImUser(@RequestBody @Valid CreateImUserInput input) { + return Result.success(imUserService.getImUser(input.getUserId(), input.getImUserType())); + } + + @IgnoreAuth + @ApiOperation(value = "更新会话扩展字段", tags = {"Mock-会话处理"}) + @PostMapping("/mock/im-conversation/update") + public Result updateImConversation(@RequestBody @Valid UpdateConversationInput input) { + imConversationService.updateConversationExtension(input); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "会话反馈", tags = {"Mock-会话处理"}) + @PostMapping("/mock/fb/v1") + public Result feedback(@RequestBody FeedbackInput input) { + aiChatFeedbackService.feedback(439058245812225L, input); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "发送Im自定义消息", tags = {"API-IM消息"}) + @PostMapping("/mock/im-message/send-ai-customer-message") + public Result sendAiCustomerMessage(@RequestBody @Valid SendAiCustomerMessageInput input) { + imMessageService.sendAiToUserCustomerMessage(input); + return Result.success(); + } + + @IgnoreAuth + @ApiOperation(value = "获取历史消息", tags = {"Mock-IM消息"}) + @PostMapping("/mock/im-message/get-history-message") + public Result> getUserToAiHistoryMessage(@RequestBody @Validated HistoryMessageInput input) { + return Result.success(imMessageService.getUserToAiHistoryMessage(input)); + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/openapi/ReceiveImMsgController.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/openapi/ReceiveImMsgController.java new file mode 100644 index 0000000..0889f0f --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/openapi/ReceiveImMsgController.java @@ -0,0 +1,91 @@ +package com.sonic.pigeon.controller.openapi; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.sonic.pigeon.domain.input.ReceiveImMsgInput; +import com.sonic.pigeon.service.CommonSendMqService; +import com.sonic.pigeon.service.ReceiveImMsgService; +import com.sonic.pigeon.utils.CheckSumBuilder; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * OPEN_API-IM回调 + */ +@Slf4j +@RestController +public class ReceiveImMsgController { + + @Value("${im.appKey}") + private String defaultAppKey; + @Autowired + private ReceiveImMsgService receiveImMsgService; + @Autowired + private CommonSendMqService commonSendMqService; + + @IgnoreAuth + @ApiOperation(value = "IM的点对点消息回调验证", tags = {"OPEN_API-IM回调"}) + @RequestMapping(value = "/open-api/webhook/receive-im-msg", produces = {"application/json"}, method = RequestMethod.POST) + public Object receiveImMsg(@Validated @RequestBody ReceiveImMsgInput input, HttpServletRequest request) throws Exception { + //参数签名验证是否通过,不通过则直接返回空 + if (!checkSignPass(input, request)) { + return null; + } + //处理如果是首次检验时触发的回调,直接快速返回成功即可 + if(StringUtils.isEmpty(input.getFromAccount()) && StringUtils.isEmpty(input.getTo())) { + return receiveImMsgService.buildSuccessResult(); + } + + log.info("===> receiveImMsg req : {}", input); + Object result = receiveImMsgService.receiveImMsgCheck(input); + log.info("===> receiveImMsg result : {}", result); + + //如果回调允许发送,则发送mq消息 + String errCode = JSONObject.parseObject(JSONObject.toJSONString(result)).getString("errCode"); + if (errCode.equals("0")) { + //发送 AI 聊天消息 + commonSendMqService.sendAiChatMq(input); + commonSendMqService.sendAiChatToFrogMq(input); + } + return result; + } + + /** + * 参数签名验证 + * @param input + * @param request + * @return + */ + private boolean checkSignPass(ReceiveImMsgInput input, HttpServletRequest request) { + String requestAppKey = request.getHeader("appkey"); + String requestMd5 = request.getHeader("MD5"); + String requestCurTime = request.getHeader("CurTime"); + String requestCheckSum = request.getHeader("CheckSum"); + log.info("===> receiveImMsg header : requestAppKey : {}, requestMd5 : {}, requestCurTime : {}, requestCheckSum : {}", requestAppKey, requestMd5, requestCurTime, requestCheckSum); + //校验值是否正确 + if (!requestAppKey.equals(defaultAppKey)) { + log.info("===> receiveImMsg check appKey error : {} , {}", requestAppKey, defaultAppKey); + return false; + } + String checkMd5 = CheckSumBuilder.getMD5(JSONObject.toJSONString(input)); + if (!requestMd5.equals(checkMd5)) { + log.info("===> receiveImMsg check md5 error : {} , {}", requestMd5, checkMd5); + return false; + } + if (StringUtils.isNotEmpty(input.getFromAccount()) && (input.getFromAccount().endsWith("@d"))) { + return false; + } + return true; + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/probe/ProbeController.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/probe/ProbeController.java new file mode 100644 index 0000000..2655e27 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/probe/ProbeController.java @@ -0,0 +1,23 @@ +package com.sonic.pigeon.controller.probe; + +import com.sonic.common.auth.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 探测接口 + */ +@RestController +@Slf4j +public class ProbeController { + + @IgnoreAuth + @ApiOperation(value = "检测服务存活状态", tags = {"探针"}) + @PostMapping("/probe/check") + public void probeCheck() { + + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/web/FeedbackWeb.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/web/FeedbackWeb.java new file mode 100644 index 0000000..7530cc8 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/web/FeedbackWeb.java @@ -0,0 +1,44 @@ +package com.sonic.pigeon.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.RedisLock; +import com.sonic.pigeon.domain.input.FeedbackInput; +import com.sonic.pigeon.service.AiChatFeedbackService; +import com.sonic.pigeon.utils.RedisKeyUtils; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Web-AI聊天反馈 + * @author coder + */ +@Validated +@RestController +public class FeedbackWeb { + + @Autowired + private AiChatFeedbackService aiChatFeedbackService; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + @ApiOperation(value = "点赞点踩反馈", tags = {"Web-AI聊天反馈"}) + @PostMapping("/web/fb/v1") + public Result feedback(@RequestBody FeedbackInput input, @ApiParam(hidden = true) Session session) { + //操作人加锁,防止并发操作 + RedisLock redisLock = new RedisLock(redisKeyUtils.feedbackLockKey(session.getUserId()), redisWrapper); + redisLock.tryAcquireRun(() -> { + aiChatFeedbackService.feedback(session.getUserId(), input); + return true; + }); + return Result.success(); + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/web/ImUserWeb.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/web/ImUserWeb.java new file mode 100644 index 0000000..a5b4a13 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/web/ImUserWeb.java @@ -0,0 +1,31 @@ +package com.sonic.pigeon.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.pigeon.domain.output.ImUserAccountOutput; +import com.sonic.pigeon.service.ImUserService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * IM用户账号 + * @author coder + */ +@Validated +@RestController +public class ImUserWeb { + + @Autowired + private ImUserService imUserService; + + @ApiOperation(value = "获取账号登录信息信息", tags = {"Web-IM用户账号"}) + @PostMapping("/web/im-user/get-account") + public Result getAccountInfo(@ApiParam(hidden = true) Session session) { + return Result.success(imUserService.getImUserAccount(session.getUserId())); + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/web/MessageWeb.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/web/MessageWeb.java new file mode 100644 index 0000000..63267cc --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/controller/web/MessageWeb.java @@ -0,0 +1,45 @@ +package com.sonic.pigeon.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Page; +import com.sonic.common.rpc.Result; +import com.sonic.pigeon.domain.input.MessageListInput; +import com.sonic.pigeon.domain.output.MessageListOutput; +import com.sonic.pigeon.domain.output.MessageStatOutput; +import com.sonic.pigeon.service.MessageService; +import com.sonic.pigeon.service.MessageStatService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Web-系统消息 + * + * @author mzc + */ +@Validated +@RestController +public class MessageWeb { + + @Autowired + private MessageStatService messageStatService; + @Autowired + private MessageService messageService; + + @ApiOperation(value = "系统消息统计", tags = {"Web-系统消息"}) + @PostMapping("/web/message/stat") + public Result messageStat(@ApiParam(hidden = true) Session session) { + return Result.success(messageStatService.messageStat(session.getUserId())); + } + + @ApiOperation(value = "系统消息列表", tags = {"Web-系统消息"}) + @PostMapping("/web/message/list") + public Result> messageList(@Validated @RequestBody MessageListInput input, @ApiParam(hidden = true) Session session) { + return Result.success(messageService.messageList(session.getUserId(), input)); + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/dao/AiChatFeedbackDao.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/dao/AiChatFeedbackDao.java new file mode 100644 index 0000000..25ec2da --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/dao/AiChatFeedbackDao.java @@ -0,0 +1,7 @@ +package com.sonic.pigeon.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.pigeon.domain.entity.AiChatFeedback; + +public interface AiChatFeedbackDao extends BaseMapper { +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/dao/MessageDao.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/dao/MessageDao.java new file mode 100644 index 0000000..4ce946a --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/dao/MessageDao.java @@ -0,0 +1,10 @@ +package com.sonic.pigeon.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.pigeon.domain.entity.Message; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MessageDao extends BaseMapper { + +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/dao/MessageStatDao.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/dao/MessageStatDao.java new file mode 100644 index 0000000..5f208b6 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/dao/MessageStatDao.java @@ -0,0 +1,19 @@ +package com.sonic.pigeon.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.pigeon.domain.entity.MessageStat; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface MessageStatDao extends BaseMapper { + + /** + * 更新未读消息数 + * + * @param userId + * @param unreadNum + * @param latestContent + */ + void updateUnread(@Param("userId") Long userId, @Param("unreadNum") int unreadNum, @Param("latestContent") String latestContent); +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/bo/ImUserBo.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/bo/ImUserBo.java new file mode 100644 index 0000000..7cc1d6e --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/bo/ImUserBo.java @@ -0,0 +1,17 @@ +package com.sonic.pigeon.domain.bo; + +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ImUserBo { + + private Long userId; + + private ImUserTypeEnum imUserType; + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/bo/PushPayloadBo.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/bo/PushPayloadBo.java new file mode 100644 index 0000000..286e87e --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/bo/PushPayloadBo.java @@ -0,0 +1,18 @@ +package com.sonic.pigeon.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +public class PushPayloadBo { + + private String fromAccountId; + + private String toAccountId; + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/entity/AiChatFeedback.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/entity/AiChatFeedback.java new file mode 100644 index 0000000..2bef9af --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/entity/AiChatFeedback.java @@ -0,0 +1,72 @@ +package com.sonic.pigeon.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * AI聊天反馈表 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_im_ai_chat_feedback") +@ApiModel(description = "AI聊天反馈表") +public class AiChatFeedback { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.NONE) + @ApiModelProperty(value = "主键ID") + private Long id; + + /** + * 消息ID + */ + @TableField("message_id") + @ApiModelProperty(value = "消息ID") + private String messageId; + + /** + * 用户ID + */ + @TableField("user_id") + @ApiModelProperty(value = "用户ID") + private Long userId; + + /** + * AI的ID + */ + @TableField("ai_id") + @ApiModelProperty(value = "AI的ID") + private Long aiId; + + /** + * 文本内容 + */ + @TableField("content") + @ApiModelProperty(value = "文本内容") + private String content; + + /** + * 操作类型(1 赞、2 踩) + */ + @TableField("opt_type") + @ApiModelProperty(value = "操作类型(1 赞、2 踩)") + private Integer optType; + + /** + * 创建时间 + */ + @TableField("create_time") + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/entity/Message.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/entity/Message.java new file mode 100644 index 0000000..28bd4d1 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/entity/Message.java @@ -0,0 +1,95 @@ +package com.sonic.pigeon.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 消息记录表 + */ +@Data +@TableName("t_message") +public class Message{ + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 发送人ID + */ + @TableField("send_user_id") + private Long sendUserId; + + /** + * 用户ID(接收人) + */ + @TableField("user_id") + private Long userId; + + /** + * 消息类型 + */ + @TableField("type") + private Integer type; + + /** + * 导致消息发送的业务ID + */ + @TableField("biz_id") + private String bizId; + + /** + * 消息状态(0未读、1已读) + */ + @TableField("status") + private Integer status; + + /** + * 消息标题 + */ + @TableField("title") + private String title; + + /** + * 消息内容 + */ + @TableField("content") + private String content; + + /** + * 消息扩展内容 + */ + @TableField("extras") + private String extras; + + /** + * 国际化翻译的替换数据 + */ + @TableField("replace_json") + private String replaceJson; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 最后修改时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; + + /** + * 是否已删除 + */ + @TableField("is_delete") + private Boolean isDelete; +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/entity/MessageStat.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/entity/MessageStat.java new file mode 100644 index 0000000..69a5e48 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/entity/MessageStat.java @@ -0,0 +1,60 @@ +package com.sonic.pigeon.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 消息统计 + */ +@Data +@TableName("t_message_stat") +public class MessageStat{ + + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 用户ID(接收人) + */ + @TableField("user_id") + private Long userId; + + /** + * 未读数量 + */ + @TableField("un_read") + private Integer unRead; + + /** + * 最新未读消息内容 + */ + @TableField("latest_content") + private String latestContent; + + /** + * 最新未读消息时间 + */ + @TableField("latest_time") + private LocalDateTime latestTime; + + /** + * 创建时间 + */ + @TableField("create_time") + private LocalDateTime createTime; + + /** + * 最后修改时间 + */ + @TableField("edit_time") + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/FeedbackInput.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/FeedbackInput.java new file mode 100644 index 0000000..de01f45 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/FeedbackInput.java @@ -0,0 +1,34 @@ +package com.sonic.pigeon.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class FeedbackInput { + + @ApiModelProperty("AI的ID") + @NotNull + private Long aiId; + + @ApiModelProperty("消息ID") + @NotEmpty + private String messageId; + + @ApiModelProperty("文本内容") + @Length(max = 500) + @NotEmpty + private String content; + + @ApiModelProperty("操作类型:空或0 无操作、1 赞、2 踩") + @NotNull + private Integer optType; + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/MessageListInput.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/MessageListInput.java new file mode 100644 index 0000000..10842e0 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/MessageListInput.java @@ -0,0 +1,24 @@ +package com.sonic.pigeon.domain.input; + +import com.sonic.common.rpc.Page; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * @description: + * @author: mzc + * @date: 2025-08-25 16:10 + **/ +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class MessageListInput { + + @NotNull + Page page = new Page<>(1, 10); +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/ReceiveImMsgInput.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/ReceiveImMsgInput.java new file mode 100644 index 0000000..be595a4 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/ReceiveImMsgInput.java @@ -0,0 +1,74 @@ +package com.sonic.pigeon.domain.input; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author zzhan + * @Date 2020/10/28 11:23 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ReceiveImMsgInput { + + private String key; + + /** 参见第三方回调事件类型,包括:点对点消息、群消息、聊天室消息、超大群消息 */ + private Integer eventType; + + /** 消息发送者的用户账号 */ + private String fromAccount; + + /** 发送方昵称 */ + private String fromNick; + + /** 发送客户端类型: AOS、IOS、PC、WINPHONE、WEB、REST */ + private String fromClientType; + + /** 发送设备id */ + private String fromDeviceId; + + /** 若eventType为1,则to为消息接收者的用户账号,字符串类型 + 若eventType为2,则to为tid,即群id,可转为Long型数据 + 若eventType为6,则to为roomid,即聊天室id,可转为Long型数据 + 若eventType为22,则to为tid,即超大群id,可转为Long型数据 */ + private String to; + + /** 消息发送时间 */ + private String msgTimestamp; + + /** TEXT :文本消息 + PICTURE :图片消息 + AUDIO :语音消息 + VIDEO :视频消息 + LOCATION :地理位置 + NOTIFICATION :通知 + FILE :文件消息 + TIPS :提示类型消息 + CUSTOM :自定义消息 */ + private String msgType; + + /** 消息发送方的客户端IP地址 */ + private String fromClientIp; + + /** 消息发送方的客户端端口号 */ + private String fromClientPort; + + /** 客户端消息Id */ + private String msgidClient; + + /** 消息内容 */ + private String body; + + /** 消息附件 */ + private String attach; + + /** 消息扩展字段 */ + private String ext; + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/SendMessageInput.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/SendMessageInput.java new file mode 100644 index 0000000..f406075 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/input/SendMessageInput.java @@ -0,0 +1,49 @@ +package com.sonic.pigeon.domain.input; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2025-08-26 10:26 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SendMessageInput { + + @ApiModelProperty(value = "发送人ID") + private Long sendUserId; + + @ApiModelProperty(value = "用户ID(接收人)") + private Long userId; + + @ApiModelProperty(value = "消息类型") + private Integer type; + + @ApiModelProperty(value = "导致消息发送的业务ID") + private String bizId; + + @ApiModelProperty(value = "消息标题") + private String title; + + @ApiModelProperty(value = "消息内容") + private String content; + + @ApiModelProperty(value = "消息扩展内容") + private String extras; + + @ApiModelProperty(value = "消息替换内容,例('nickname','email')") + private List titleReplaceList = new ArrayList<>(); + @ApiModelProperty(value = "消息替换内容,例('nickname','email')") + private List contentReplaceList = new ArrayList<>(); + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/ImUserAccountOutput.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/ImUserAccountOutput.java new file mode 100644 index 0000000..4e23c46 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/ImUserAccountOutput.java @@ -0,0 +1,19 @@ +package com.sonic.pigeon.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ImUserAccountOutput { + + @ApiModelProperty("账号ID") + private String accountId; + + @ApiModelProperty("登录凭证") + private String token; + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/MessageListOutput.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/MessageListOutput.java new file mode 100644 index 0000000..c70ee6b --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/MessageListOutput.java @@ -0,0 +1,51 @@ +package com.sonic.pigeon.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-08-25 16:11 + **/ +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class MessageListOutput { + + @ApiModelProperty("消息ID") + private Long id; + + @ApiModelProperty("发送人用户ID") + private Long sendUserId; + + @ApiModelProperty("消息类型") + private Integer type; + + @ApiModelProperty("导致消息发送的业务ID") + private String bizId; + + @ApiModelProperty("消息状态(0未读、1已读)") + private Integer status; + + @ApiModelProperty("消息标题") + private String title; + + @ApiModelProperty("消息内容") + private String content; + + @ApiModelProperty("消息扩展内容") + private String extras; + + @ApiModelProperty("国家化翻译替换数据") + private String replaceJson; + + @ApiModelProperty("消息时间") + private LocalDateTime createTime; +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/MessageStatOutput.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/MessageStatOutput.java new file mode 100644 index 0000000..c9f0c79 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/MessageStatOutput.java @@ -0,0 +1,30 @@ +package com.sonic.pigeon.domain.output; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @description: + * @author: mzc + * @date: 2025-08-25 15:58 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MessageStatOutput { + + @ApiModelProperty("未读数量") + private Integer unRead = 0; + + @ApiModelProperty("最新未读消息内容") + private String latestContent; + + @ApiModelProperty("最新未读消息时间") + private LocalDateTime latestTime; +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/UserCommonlyUsedLogOutput.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/UserCommonlyUsedLogOutput.java new file mode 100644 index 0000000..70be0bd --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/domain/output/UserCommonlyUsedLogOutput.java @@ -0,0 +1,35 @@ +package com.sonic.pigeon.domain.output; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author code + * @Description 用户常用日志 + * @Date 2024/1/12 11:57 + * @Version 1.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserCommonlyUsedLogOutput { + + /** + * 用户ID + */ + private Long userId; + + /** + * 数据类型(1 IP地址、2 设备好) + */ + private Integer dataType; + + /** + * 数据值 + */ + private String dataValue; + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/BizResultCode.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/BizResultCode.java new file mode 100644 index 0000000..3dd2dd6 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/BizResultCode.java @@ -0,0 +1,106 @@ +package com.sonic.pigeon.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum BizResultCode implements ApiResultCode { + + /** + * 可以在此处扩展服务自身需要用到的错误码信息 + */ + SYS_PARAMETERS_VALIDATE_EXCEPTION("10010003", "参数验证错误"), + SYS_VALIDATION_FAILED_ERROR("1001011", "验证失败,无效请求"), + + ACCOUNT_NOT_REGISTER("-1", "Incorrect username or password"), + FACEBOOK_ACCOUNT_ERROR("-1", "Facebook account could not be verified"), + FACEBOOK_INVALID("-1", "Facebook login is invalid, please login again"), + + APPLE_NETWORK_ERROR("10000001", "apple network error"), + + MISS_PARAM_ERROR("1001010", "Missing parameter"), + + DISCORD_NETWORK_ERROR("10000001", "discord network error"), + GOOGLE_ID_GET_ERROR("10010151", "GOOGLE ID GET ERROR"), + + AUTH_FAIL("10010151", "授权失败"), + + DEVICE_BLOCK_ERROR("10010152", "该设备已被封禁"), + + FREEZE_ERROR("10010153", "账号已被冻结"), + + ; + + + private final String errorCode; + private final String errorMsg; + + BizResultCode(String errorCode, String errorMsg) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1002"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + + /** + * 校验方法 + * + * @param expect + * @param code + */ + public void check(boolean expect, String code) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(code, this.name().equals(message) ? this.getErrorMsg() : message); + + } + } + + /** + * 校验方法 + * + * @param expect + * @param code + * @param message + */ + public void check(boolean expect, String code, String message) { + if (expect) { + throw new BizException(code, message); + } + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/BusinessException.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/BusinessException.java new file mode 100644 index 0000000..c907a1b --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/BusinessException.java @@ -0,0 +1,39 @@ +package com.sonic.pigeon.enums; + + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.GlobalResultCode; + +public class BusinessException extends BizException { + + private static final long serialVersionUID = -5317007026578376164L; + + /** + * 错误码 + */ + private String errorCode; + /** + * 错误描述 + */ + private String errorMsg; + + /** + * @param errorCode + * @param errorMsg + */ + public BusinessException(String errorCode, String errorMsg) { + super(GlobalResultCode.INVALID_PARAMS.getErrorCode(), String.format("BusinessException{errorCode:%s, errorMsg:%s}", errorCode, errorMsg)); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + @Override + public String getErrorCode() { + return errorCode; + } + + @Override + public String getErrorMsg() { + return errorMsg; + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/MessageStatusEnum.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/MessageStatusEnum.java new file mode 100644 index 0000000..e5b0a5c --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/MessageStatusEnum.java @@ -0,0 +1,24 @@ +package com.sonic.pigeon.enums; + +import lombok.Getter; + +/** + * @author stage + */ + +@Getter +public enum MessageStatusEnum { + + //未读 + NO_READ(0, "Unread"), + //已读 + READ(1, "Read"); + + private final Integer index; + private final String name; + + MessageStatusEnum(Integer index, String name) { + this.index = index; + this.name = name; + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/ToastResultCode.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/ToastResultCode.java new file mode 100644 index 0000000..2f3a7ca --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/enums/ToastResultCode.java @@ -0,0 +1,91 @@ +package com.sonic.pigeon.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum ToastResultCode implements ApiResultCode { + + /** 可以在此处扩展服务自身需要用到的错误码信息 */ + NO_LOGIN("0001", "User is not logged in"), + USER_NOT_EXIST("0002", "User does not exist"), + APP_CLIENT_NOT_EXIST("0003", "Login client does not exist"), + APP_CLIENT_NOT_ALLOW("0004", "Not authorized to log in to the client"), + SESSION_EXPIRED("0005", "Session is expired"), + SESSION_INVALID("0006", "Session is invalid"), + PASSWORD_INVALID("0007", "Invalid account or password"), + /** 第三方登陆授权失败 */ + AUTH_FAIL("0008","auth.fail"), + USER_FROZEN("0009", "User is frozen"), + + ; + + + private final String errorCode; + private final String errorMsg; + + ToastResultCode(String errorMsg) { + this.errorCode = ""; + this.errorMsg = errorMsg; + } + + ToastResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect, String msg) { + if (expect) { + if(StringUtils.isNotBlank(msg)) { + throw new BizException(this.getErrorCode(), msg); + + } else { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/inner/EventType.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/inner/EventType.java new file mode 100644 index 0000000..026cfa4 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/inner/EventType.java @@ -0,0 +1,31 @@ +package com.sonic.pigeon.event.inner; + +import com.sonic.pigeon.config.EventConfig; +import com.sonic.common.event.Event; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义当前系统的消息 + * @author coder + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** 事件定义 */ + DEMO_CREATED(EventConfig.DEFAULT_SCENE, EventConfig.PIGEON, "demo_created", "demo 创建"), + + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/outer/EventType.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/outer/EventType.java new file mode 100644 index 0000000..cc3a570 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/outer/EventType.java @@ -0,0 +1,37 @@ +package com.sonic.pigeon.event.outer; + +import com.sonic.common.event.Event; +import com.sonic.pigeon.config.EventConfig; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义不属于当前系统的消息 + * @author coder + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** 事件定义 */ + USER_CREATED(Event.BuildInScene.BS.getCode(), "bs_user", "user_created", "用户创建"), + + AI_CHAT(EventConfig.DEFAULT_SCENE, EventConfig.PIGEON, "ai_chat", "和AI聊天"), + + USER_BALANCE_INSUFFICIENT_CHECKOUT(EventConfig.DEFAULT_SCENE, EventConfig.FROG, "user_balance_insufficient_checkout", "余额不足,文本,语音,语音通话预扣款结算"), + + AI_CHAT_TO_FROG(EventConfig.DEFAULT_SCENE, EventConfig.FROG, "ai_chat_to_frog", "AI聊天同步到业务系统"), + + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/outer/payload/AiChatPayload.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/outer/payload/AiChatPayload.java new file mode 100644 index 0000000..b737c80 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/outer/payload/AiChatPayload.java @@ -0,0 +1,32 @@ +package com.sonic.pigeon.event.outer.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author coder + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiChatPayload { + + private Long fromUserId; + + private Long toUserId; + + private String content; + + /** + * 消息类型 + */ + private String messageType; + + /** 消息附件 */ + private String attach; + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/outer/payload/UserBalanceInsufficientCheckoutPayload.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/outer/payload/UserBalanceInsufficientCheckoutPayload.java new file mode 100644 index 0000000..8da399d --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/event/outer/payload/UserBalanceInsufficientCheckoutPayload.java @@ -0,0 +1,22 @@ +package com.sonic.pigeon.event.outer.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author coder + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserBalanceInsufficientCheckoutPayload { + + /** + * 用户id + */ + private Long userId; +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/AiChatFeedbackService.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/AiChatFeedbackService.java new file mode 100644 index 0000000..1616a2f --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/AiChatFeedbackService.java @@ -0,0 +1,19 @@ +package com.sonic.pigeon.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.pigeon.domain.entity.AiChatFeedback; +import com.sonic.pigeon.domain.input.FeedbackInput; + +/** + * AI聊天反馈 + */ +public interface AiChatFeedbackService extends IService { + + /** + * 反馈 + * @param userId + * @param input + */ + void feedback(Long userId, FeedbackInput input); + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/CommonSendMqService.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/CommonSendMqService.java new file mode 100644 index 0000000..ed8f5b9 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/CommonSendMqService.java @@ -0,0 +1,52 @@ +package com.sonic.pigeon.service; + +import com.sonic.pigeon.domain.input.ReceiveImMsgInput; + +/** + * @description: 发送消息到mq + * @author: zhenqiang.zhan + * @create: 2020-02-06 17:56 + **/ +public interface CommonSendMqService { + + /** + * 发送AI聊天的MQ消息 + * + * @param input + */ + void sendAiChatMq(ReceiveImMsgInput input); + + /** + * 发送AI聊天的MQ消息 + * + * @param input + */ + void sendAiChatToFrogMq(ReceiveImMsgInput input); + + /** + * 发送AI聊天的MQ消息 + * + * @param fromUserId + * @param toUserId + * @param content + */ + void sendAiChatMqV1(Long fromUserId, Long toUserId, String content); + + /** + * 发送AI聊天的MQ消息 + * + * @param fromUserId + * @param toUserId + * @param content + */ + void sendAiChatMqV2(Long fromUserId, Long toUserId, String content); + + /** + * 用户余额不足,发起扣款mq + * + * @param userId + */ + void userBalanceInsufficientCheckoutMq(Long userId); + + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ImConversationService.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ImConversationService.java new file mode 100644 index 0000000..261cfb5 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ImConversationService.java @@ -0,0 +1,23 @@ +package com.sonic.pigeon.service; + +import com.sonic.pigeon.lib.input.CreateConversationInput; +import com.sonic.pigeon.lib.input.UpdateConversationInput; + +/** + * Im会话处理 + */ +public interface ImConversationService { + + /** + * 创建会话 + * @param input + */ + void createConversation(CreateConversationInput input); + + /** + * 更新会话扩展信息 + * @param input + */ + void updateConversationExtension(UpdateConversationInput input); + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ImMessageService.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ImMessageService.java new file mode 100644 index 0000000..0cbbc7a --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ImMessageService.java @@ -0,0 +1,85 @@ +package com.sonic.pigeon.service; + +import com.netease.nim.server.sdk.im.v2.message.response.QueryMessageResponseV2; +import com.sonic.pigeon.lib.bo.AttachBo; +import com.sonic.pigeon.lib.input.HistoryMessageInput; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import com.sonic.pigeon.lib.input.SendAiTextMessageInput; +import com.sonic.pigeon.lib.output.HistoryMessageOutput; + +import java.util.List; +import java.util.Map; + +public interface ImMessageService { + + /** + * 发送AI文本消息 + * @param input + */ + void sendAiToUserTextMessage(SendAiTextMessageInput input); + + /** + * 发送AI文本消息 + * @param input + */ + void sendUserToAiTextMessage(SendAiTextMessageInput input); + + /** + * 发送Im自定义消息 + * @param input + */ + void sendAiToUserCustomerMessage(SendAiCustomerMessageInput input); + + /** + * 发送Im自定义消息 + * @param input + */ + void sendUserToAiCustomerMessage(SendAiCustomerMessageInput input); + + /** + * 发送用户消息 + * @param input + */ + void sendUserTextMessage(SendAiTextMessageInput input); + + /** + * 获取历史消息 + * @param input + * @return + */ + List getUserToAiHistoryMessage(HistoryMessageInput input); + + /** + * 获取历史消息 + * @param input + * @return + */ + List getAiToUserHistoryMessage(HistoryMessageInput input); + + /** + * 获取AI发送的消息明细 + * @param aiId + * @param userId + * @param messageServerId + * @return + */ + QueryMessageResponseV2 getAiSendMessage(Long aiId, Long userId, Long messageServerId); + + /** + * 更新AI发送的图片消息 + * @param aiId + * @param userId + * @param messageServerId + * @param attachment + */ + void updateAiSendCustomImageMessage(Long aiId, Long userId, Long messageServerId, AttachBo attachment); + + /** + * 更新AI发送的消息扩展字段,追加扩展内容 + * @param aiId + * @param userId + * @param messageServerId + * @param extension + */ + void updateAiSendMessage(Long aiId, Long userId, Long messageServerId, Map extension); +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ImUserService.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ImUserService.java new file mode 100644 index 0000000..2d31027 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ImUserService.java @@ -0,0 +1,40 @@ +package com.sonic.pigeon.service; + +import com.netease.nim.server.sdk.im.v2.users.response.GetUserResponseV2; +import com.sonic.pigeon.domain.output.ImUserAccountOutput; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import com.sonic.pigeon.lib.input.CreateImUserInput; +import com.sonic.pigeon.lib.input.EditImUserInput; + +/** + * Im用户处理 + */ +public interface ImUserService { + + /** + * 创建用户 + * @param input + */ + void createImUser(CreateImUserInput input); + + /** + * 修改用户信息 + * @param input + */ + void editImUser(EditImUserInput input); + + /** + * 获取im用户信息 + * @param userId + * @param imUserType + * @return + */ + GetUserResponseV2 getImUser(Long userId, ImUserTypeEnum imUserType); + + /** + * 获取IM用户登录信息 + * @param userId + * @return + */ + ImUserAccountOutput getImUserAccount(Long userId); +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/MessageService.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/MessageService.java new file mode 100644 index 0000000..9be54cc --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/MessageService.java @@ -0,0 +1,28 @@ +package com.sonic.pigeon.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.common.rpc.Page; +import com.sonic.pigeon.domain.entity.Message; +import com.sonic.pigeon.domain.input.MessageListInput; +import com.sonic.pigeon.domain.input.SendMessageInput; +import com.sonic.pigeon.domain.output.MessageListOutput; + +import java.util.List; + +public interface MessageService extends IService { + + /** + * 发送消息 + * @param reqList + */ + void sendMessage(List reqList); + + /** + * 消息列表 + * + * @param userId + * @param input + * @return + */ + Page messageList(Long userId, MessageListInput input); +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/MessageStatService.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/MessageStatService.java new file mode 100644 index 0000000..139ed2a --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/MessageStatService.java @@ -0,0 +1,28 @@ +package com.sonic.pigeon.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.pigeon.domain.entity.MessageStat; +import com.sonic.pigeon.domain.output.MessageStatOutput; + +import java.time.LocalDateTime; + +public interface MessageStatService extends IService { + + /** + * 获取用户消息统计 + * + * @param userId + * @return + */ + MessageStatOutput messageStat(Long userId); + + /** + * 更新未读消息 + * 添加新消息时 加未读消息数 + * 已阅读 减未读消息数 + * + * @param userId + * @param unreadNum 未读消息数 + */ + void updateUnread(Long userId, int unreadNum, String latestContent); +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ReceiveImMsgService.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ReceiveImMsgService.java new file mode 100644 index 0000000..da5488e --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/ReceiveImMsgService.java @@ -0,0 +1,28 @@ +package com.sonic.pigeon.service; + + +import com.sonic.pigeon.domain.input.ReceiveImMsgInput; + +import java.util.Map; + +/** + * @Author zzhan + * @Date 2020/10/28 11:19 + * @Version 1.0 + */ +public interface ReceiveImMsgService { + + /** + * 接收IM消息的校验 + * + * @param input + * @return + */ + Object receiveImMsgCheck(ReceiveImMsgInput input); + + /** + * 构建成功结果 + * @return + */ + Map buildSuccessResult(); +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/AiChatFeedbackServiceImpl.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/AiChatFeedbackServiceImpl.java new file mode 100644 index 0000000..b0a491c --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/AiChatFeedbackServiceImpl.java @@ -0,0 +1,72 @@ +package com.sonic.pigeon.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.ImmutableMap; +import com.sonic.pigeon.dao.AiChatFeedbackDao; +import com.sonic.pigeon.domain.entity.AiChatFeedback; +import com.sonic.pigeon.domain.input.FeedbackInput; +import com.sonic.pigeon.service.AiChatFeedbackService; +import com.sonic.pigeon.service.ImMessageService; +import com.sonic.pigeon.utils.LimitUtils; +import com.sonic.pigeon.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +/** + * AI聊天反馈 + */ + +@Slf4j +@Service +public class AiChatFeedbackServiceImpl extends ServiceImpl implements AiChatFeedbackService { + + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private LimitUtils limitUtils; + @Autowired + private ImMessageService imMessageService; + + @Override + public void feedback(Long userId, FeedbackInput input) { + //上锁、限流 + boolean bl = limitUtils.defaultLimitCheckByKey(redisKeyUtils.feedbackLimitKey(userId), 200, 24 * 60 * 60); + if(bl) { + //限流了,则不再进行处理 + return; + } + try { + AiChatFeedback feedback = getOne(Wrappers.lambdaQuery().eq(AiChatFeedback::getMessageId, input.getMessageId())); + if(feedback == null) { + //保存数据 + feedback = new AiChatFeedback(); + feedback.setUserId(userId); + feedback.setAiId(input.getAiId()); + feedback.setContent(input.getContent()); + feedback.setMessageId(input.getMessageId()); + feedback.setOptType(input.getOptType()); + feedback.setCreateTime(LocalDateTime.now()); + save(feedback); + } else { + //校验操作用户是否匹配,不匹配直接快速返回 + if(!feedback.getUserId().equals(userId)) { + return; + } + //更新状态 + update(Wrappers.lambdaUpdate().set(AiChatFeedback::getOptType, input.getOptType()).eq(AiChatFeedback::getId, feedback.getId())); + } + if(input.getMessageId().startsWith("prologue_")) { + return; + } + //更新im消息 + imMessageService.updateAiSendMessage(input.getAiId(), userId, Long.valueOf(input.getMessageId()), ImmutableMap.of("optType", input.getOptType())); + } catch (Exception e) { + log.error("===> feedback save error : ", e); + } + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/CommonSendMqServiceImpl.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/CommonSendMqServiceImpl.java new file mode 100644 index 0000000..d0ba8f1 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/CommonSendMqServiceImpl.java @@ -0,0 +1,137 @@ +package com.sonic.pigeon.service.impl; + +import com.sonic.common.event.Event; +import com.sonic.common.event.EventProducer; +import com.sonic.common.event.RabbitmqEventProducer; +import com.sonic.pigeon.domain.bo.ImUserBo; +import com.sonic.pigeon.domain.input.ReceiveImMsgInput; +import com.sonic.pigeon.event.outer.payload.AiChatPayload; +import com.sonic.pigeon.event.outer.payload.UserBalanceInsufficientCheckoutPayload; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import com.sonic.pigeon.lib.enums.MessageTypeEnum; +import com.sonic.pigeon.service.CommonSendMqService; +import com.sonic.pigeon.utils.ImGenerator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +import static com.sonic.pigeon.event.outer.EventType.*; + +/** + * 发送消息到mq + * + * @Author zzhan + * @Date 2022/3/2 + * @Version 1.0 + */ +@Slf4j +@Service +public class CommonSendMqServiceImpl implements CommonSendMqService { + + @Autowired + private EventProducer eventProducer; + + @Autowired + private ImGenerator imGenerator; + + @Autowired + @Qualifier("aiChatMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta aiChatMeta; + @Autowired + @Qualifier("aiChatToFrogMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta aiChatToFrogMeta; + @Autowired + @Qualifier("userBalanceInsufficientCheckoutMeta") + private RabbitmqEventProducer.RabbitmqMessageMeta userBalanceInsufficientCheckoutMeta; + + + @Override + public void sendAiChatMq(ReceiveImMsgInput input) { + //处理逻辑,判断发送人。接收人 + ImUserBo fromUserBo = imGenerator.getImUserBo(input.getFromAccount()); + ImUserBo toUserBo = imGenerator.getImUserBo(input.getTo()); + //只有普通用户给AI机器人发送消息 才进行处理 + if(ImUserTypeEnum.u != fromUserBo.getImUserType() && ImUserTypeEnum.r != toUserBo.getImUserType()) { + //快速返回,不进行处理 + return; + } + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(AI_CHAT.getEventCode().getScene()) + .eventModule(AI_CHAT.getEventCode().getModule()) + .eventName(AI_CHAT.getEventCode().getName()) + .data(AiChatPayload.builder() + .fromUserId(fromUserBo.getUserId()) + .toUserId(toUserBo.getUserId()) + .messageType(input.getMsgType()) + .content(input.getBody()) + .attach(input.getAttach()) + .build()).build(), aiChatMeta); + } + + @Override + public void sendAiChatToFrogMq(ReceiveImMsgInput input) { + //处理逻辑,判断发送人。接收人 + ImUserBo fromUserBo = imGenerator.getImUserBo(input.getFromAccount()); + ImUserBo toUserBo = imGenerator.getImUserBo(input.getTo()); + //只有普通用户给AI机器人发送消息 才进行处理 + if(ImUserTypeEnum.u != fromUserBo.getImUserType() && ImUserTypeEnum.r != toUserBo.getImUserType()) { + //快速返回,不进行处理 + return; + } + eventProducer.send(Event.builder() + .eventScene(AI_CHAT_TO_FROG.getEventCode().getScene()) + .eventModule(AI_CHAT_TO_FROG.getEventCode().getModule()) + .eventName(AI_CHAT_TO_FROG.getEventCode().getName()) + .data(AiChatPayload.builder() + .fromUserId(fromUserBo.getUserId()) + .toUserId(toUserBo.getUserId()) + .messageType(input.getMsgType()) + .content(input.getBody()) + .attach(input.getAttach()) + .build()).build(), aiChatToFrogMeta); + } + + @Override + public void sendAiChatMqV1(Long fromUserId, Long toUserId, String content) { + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(AI_CHAT.getEventCode().getScene()) + .eventModule(AI_CHAT.getEventCode().getModule()) + .eventName(AI_CHAT.getEventCode().getName()) + .data(AiChatPayload.builder() + .fromUserId(fromUserId) + .toUserId(toUserId) + .messageType(MessageTypeEnum.TEXT.name()) + .content(content) + .build()).build(), aiChatMeta); + } + + @Override + public void sendAiChatMqV2(Long fromUserId, Long toUserId, String content) { + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(AI_CHAT.getEventCode().getScene()) + .eventModule(AI_CHAT.getEventCode().getModule()) + .eventName(AI_CHAT.getEventCode().getName()) + .data(AiChatPayload.builder() + .fromUserId(fromUserId) + .toUserId(toUserId) + .messageType(MessageTypeEnum.CUSTOM.name()) + .content(content) + .attach("{\"type\":\"image\",\"url\":\"https://hhb.crushlevel.ai/dev/main/role/439058245812225/17557485208684304.png\",\"width\":800,\"height\":960}") + .build()).build(), aiChatMeta); + } + + @Override + public void userBalanceInsufficientCheckoutMq(Long userId) { + //发送MQ消息 + eventProducer.send(Event.builder() + .eventScene(USER_BALANCE_INSUFFICIENT_CHECKOUT.getEventCode().getScene()) + .eventModule(USER_BALANCE_INSUFFICIENT_CHECKOUT.getEventCode().getModule()) + .eventName(USER_BALANCE_INSUFFICIENT_CHECKOUT.getEventCode().getName()) + .data(UserBalanceInsufficientCheckoutPayload.builder() + .userId(userId).build()).build(), userBalanceInsufficientCheckoutMeta); + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ImConversationServiceImpl.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ImConversationServiceImpl.java new file mode 100644 index 0000000..589a9e5 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ImConversationServiceImpl.java @@ -0,0 +1,74 @@ +package com.sonic.pigeon.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.netease.nim.server.sdk.core.Result; +import com.netease.nim.server.sdk.core.exception.YunxinSdkException; +import com.netease.nim.server.sdk.im.v2.YunxinV2ApiServices; +import com.netease.nim.server.sdk.im.v2.conversation.request.CreateConversationRequestV2; +import com.netease.nim.server.sdk.im.v2.conversation.request.UpdateConversationRequestV2; +import com.netease.nim.server.sdk.im.v2.conversation.response.CreateConversationResponseV2; +import com.netease.nim.server.sdk.im.v2.conversation.response.UpdateConversationResponseV2; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import com.sonic.pigeon.lib.input.CreateConversationInput; +import com.sonic.pigeon.lib.input.UpdateConversationInput; +import com.sonic.pigeon.service.ImConversationService; +import com.sonic.pigeon.utils.ImGenerator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class ImConversationServiceImpl implements ImConversationService { + + @Autowired + private YunxinV2ApiServices yunxinV2ApiServices; + @Autowired + private ImGenerator imGenerator; + + @Override + public void createConversation(CreateConversationInput input) { + CreateConversationRequestV2 request = new CreateConversationRequestV2(); + String fromAccountId = imGenerator.genImAccountId(input.getFromUserId(), ImUserTypeEnum.u); + String toAccountId = imGenerator.genImAccountId(input.getToUserId(), ImUserTypeEnum.r); + request.setConversationId(fromAccountId + "|1|" + toAccountId); + try { + Result result = yunxinV2ApiServices.getConversationService().createConversation(request); + if (result.isSuccess()) { + CreateConversationResponseV2 response = result.getResponse(); + // 更新成功 + log.info("===> createConversation success, response : {}, traceId : {}", JSONObject.toJSONString(response), result.getTraceId()); + } else { + // 更新失败,如参数错误等 + log.info("===> createConversation fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> createConversation error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + } + + @Override + public void updateConversationExtension(UpdateConversationInput input) { + UpdateConversationRequestV2 request = new UpdateConversationRequestV2(); + String fromAccountId = imGenerator.genImAccountId(input.getFromUserId(), ImUserTypeEnum.u); + String toAccountId = imGenerator.genImAccountId(input.getToUserId(), ImUserTypeEnum.r); + request.setConversationId(fromAccountId + "|1|" + toAccountId); + request.setServerExtension(input.getExtension()); + try { + Result result = yunxinV2ApiServices.getConversationService().updateConversation(request); + if (result.isSuccess()) { + UpdateConversationResponseV2 response = result.getResponse(); + // 更新成功 + log.info("===> updateConversationExtension success, response : {}, traceId : {}", JSONObject.toJSONString(response), result.getTraceId()); + } else { + // 更新失败,如参数错误等 + log.info("===> updateConversationExtension fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> updateConversationExtension error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ImMessageServiceImpl.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ImMessageServiceImpl.java new file mode 100644 index 0000000..24d1b50 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ImMessageServiceImpl.java @@ -0,0 +1,351 @@ +package com.sonic.pigeon.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.netease.nim.server.sdk.core.Result; +import com.netease.nim.server.sdk.core.exception.YunxinSdkException; +import com.netease.nim.server.sdk.im.v2.YunxinV2ApiServices; +import com.netease.nim.server.sdk.im.v2.message.request.ModifyMessageRequestV2; +import com.netease.nim.server.sdk.im.v2.message.request.QueryMessageRequestV2; +import com.netease.nim.server.sdk.im.v2.message.request.QueryMessagesByPageRequestV2; +import com.netease.nim.server.sdk.im.v2.message.request.SendMessageRequestV2; +import com.netease.nim.server.sdk.im.v2.message.response.ModifyMessageResponseV2; +import com.netease.nim.server.sdk.im.v2.message.response.QueryMessageResponseV2; +import com.netease.nim.server.sdk.im.v2.message.response.QueryMessagesByPageResponseV2; +import com.netease.nim.server.sdk.im.v2.message.response.SendMessageResponseV2; +import com.sonic.pigeon.domain.bo.PushPayloadBo; +import com.sonic.pigeon.lib.bo.AttachBo; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import com.sonic.pigeon.lib.enums.MessageTypeEnum; +import com.sonic.pigeon.lib.input.HistoryMessageInput; +import com.sonic.pigeon.lib.input.SendAiCustomerMessageInput; +import com.sonic.pigeon.lib.input.SendAiTextMessageInput; +import com.sonic.pigeon.lib.output.HistoryMessageOutput; +import com.sonic.pigeon.service.ImMessageService; +import com.sonic.pigeon.utils.ImGenerator; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class ImMessageServiceImpl implements ImMessageService { + + @Autowired + private YunxinV2ApiServices yunxinV2ApiServices; + @Autowired + private ImGenerator imGenerator; + + @Override + public void sendAiToUserTextMessage(SendAiTextMessageInput input) { + String fromAccountId = imGenerator.genImAccountId(input.getFromUserId(), ImUserTypeEnum.r); + String toAccountId = imGenerator.genImAccountId(input.getToUserId(), ImUserTypeEnum.u); + sendTextMessage(fromAccountId, toAccountId, input); + } + + @Override + public void sendUserToAiTextMessage(SendAiTextMessageInput input) { + String fromAccountId = imGenerator.genImAccountId(input.getFromUserId(), ImUserTypeEnum.u); + String toAccountId = imGenerator.genImAccountId(input.getToUserId(), ImUserTypeEnum.r); + sendTextMessage(fromAccountId, toAccountId, input); + } + + @Override + public void sendAiToUserCustomerMessage(SendAiCustomerMessageInput input) { + String fromAccountId = imGenerator.genImAccountId(input.getFromUserId(), ImUserTypeEnum.r); + String toAccountId = imGenerator.genImAccountId(input.getToUserId(), ImUserTypeEnum.u); + sendCustomerMessage(fromAccountId, toAccountId, input); + } + + @Override + public void sendUserToAiCustomerMessage(SendAiCustomerMessageInput input) { + String fromAccountId = imGenerator.genImAccountId(input.getFromUserId(), ImUserTypeEnum.u); + String toAccountId = imGenerator.genImAccountId(input.getToUserId(), ImUserTypeEnum.r); + sendCustomerMessage(fromAccountId, toAccountId, input); + } + + /** + * 发送普通文本消息 + * @param fromAccountId + * @param toAccountId + * @param input + */ + private void sendTextMessage(String fromAccountId, String toAccountId, SendAiTextMessageInput input) { + SendMessageRequestV2 request = new SendMessageRequestV2(); + request.setConversationId(fromAccountId + "|1|" + toAccountId); + SendMessageRequestV2.MessageBody messageBody = new SendMessageRequestV2.MessageBody(); + messageBody.setMessageType(MessageTypeEnum.TEXT.getCode()); + messageBody.setText(input.getContent()); + request.setMessage(messageBody); + request.setExtension(input.getExtension()); + //设置消息推送 + SendMessageRequestV2.PushConfig pushConfig = new SendMessageRequestV2.PushConfig(); + pushConfig.setPushEnabled(true); + pushConfig.setPushContent(input.getContent()); + pushConfig.setPushPayload(JSONObject.toJSONString(PushPayloadBo.builder().fromAccountId(fromAccountId).toAccountId(toAccountId).build())); + request.setPushConfig(pushConfig); + try { + Result result = yunxinV2ApiServices.getMessageService().sendMessage(request); + if (result.isSuccess()) { + // 获取成功 + log.info("===> sendAiTextMessage success, response : {}, traceId : {}", JSONObject.toJSONString(result.getResponse()), result.getTraceId()); + } else { + // 获取失败 + log.info("===> sendAiTextMessage fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> sendAiTextMessage error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + } + + /** + * 发送自定义消息 + * @param input + */ + private void sendCustomerMessage(String fromAccountId, String toAccountId, SendAiCustomerMessageInput input) { + SendMessageRequestV2 request = new SendMessageRequestV2(); + request.setConversationId(fromAccountId + "|1|" + toAccountId); + SendMessageRequestV2.MessageBody messageBody = new SendMessageRequestV2.MessageBody(); + messageBody.setMessageType(MessageTypeEnum.CUSTOM.getCode()); + messageBody.setText(input.getContent()); + //设置消息扩展字段 + messageBody.setAttachment(input.getAttachment() == null ? null : JSONObject.parseObject(input.getAttachment(), Map.class)); + //服务端扩展字段 + request.setExtension(input.getExtension()); + request.setMessage(messageBody); + //设置消息推送 + SendMessageRequestV2.PushConfig pushConfig = new SendMessageRequestV2.PushConfig(); + pushConfig.setPushEnabled(true); + pushConfig.setPushPayload(JSONObject.toJSONString(PushPayloadBo.builder().fromAccountId(fromAccountId).toAccountId(toAccountId).build())); + request.setPushConfig(pushConfig); + try { + log.info("===> sendAiCustomerMessage request : {}", JSONObject.toJSONString(request)); + Result result = yunxinV2ApiServices.getMessageService().sendMessage(request); + if (result.isSuccess()) { + // 获取成功 + log.info("===> sendAiCustomerMessage success, response : {}, traceId : {}", JSONObject.toJSONString(result.getResponse()), result.getTraceId()); + } else { + // 获取失败 + log.info("===> sendAiCustomerMessage fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> sendAiCustomerMessage error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + } + + @Override + public void sendUserTextMessage(SendAiTextMessageInput input) { + String fromAccountId = imGenerator.genImAccountId(input.getFromUserId(), ImUserTypeEnum.u); + String toAccountId = imGenerator.genImAccountId(input.getToUserId(), ImUserTypeEnum.r); + + SendMessageRequestV2 request = new SendMessageRequestV2(); + request.setConversationId(fromAccountId.toLowerCase() + "|1|" + toAccountId.toLowerCase()); + SendMessageRequestV2.MessageBody messageBody = new SendMessageRequestV2.MessageBody(); + messageBody.setMessageType(MessageTypeEnum.TEXT.getCode()); + messageBody.setText(input.getContent()); + request.setMessage(messageBody); + //设置消息推送 + SendMessageRequestV2.PushConfig pushConfig = new SendMessageRequestV2.PushConfig(); + pushConfig.setPushEnabled(true); + pushConfig.setPushContent(input.getContent()); + pushConfig.setPushPayload(JSONObject.toJSONString(PushPayloadBo.builder().fromAccountId(fromAccountId).toAccountId(toAccountId).build())); + request.setPushConfig(pushConfig); + try { + log.info("===> sendAiTextMessage param : {}", JSONObject.toJSONString(request)); + Result result = yunxinV2ApiServices.getMessageService().sendMessage(request); + if (result.isSuccess()) { + // 获取成功 + log.info("===> sendAiTextMessage success, response : {}, traceId : {}", JSONObject.toJSONString(result.getResponse()), result.getTraceId()); + } else { + // 获取失败 + log.info("===> sendAiTextMessage fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> sendAiTextMessage error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + } + + @Override + public List getUserToAiHistoryMessage(HistoryMessageInput input) { + String fromAccountId = imGenerator.genImAccountId(input.getFromUserId(), ImUserTypeEnum.u); + String toAccountId = imGenerator.genImAccountId(input.getToUserId(), ImUserTypeEnum.r); + return getHistoryMessage(fromAccountId, toAccountId, input); + } + + @Override + public List getAiToUserHistoryMessage(HistoryMessageInput input) { + String fromAccountId = imGenerator.genImAccountId(input.getFromUserId(), ImUserTypeEnum.r); + String toAccountId = imGenerator.genImAccountId(input.getToUserId(), ImUserTypeEnum.u); + return getHistoryMessage(fromAccountId, toAccountId, input); + } + + /** + * 获取历史消息 + * @param fromAccountId + * @param toAccountId + * @param input + * @return + */ + private List getHistoryMessage(String fromAccountId, String toAccountId, HistoryMessageInput input) { + QueryMessagesByPageRequestV2 request = new QueryMessagesByPageRequestV2(); + request.setConversationId(fromAccountId + "|1|" + toAccountId); + StringBuilder sb = new StringBuilder(); + sb.append(MessageTypeEnum.TEXT.getCode()); + sb.append(","); + sb.append(MessageTypeEnum.CUSTOM.getCode()); + request.setMessageType(sb.toString()); + //最近一年的消息 + request.setBeginTime(input.getBeginTime() == null ? System.currentTimeMillis() - 31557600000L : input.getBeginTime()); + request.setEndTime(input.getEndTime() == null ? System.currentTimeMillis() : input.getEndTime()); + request.setLimit(input.getLimit() == null ? 100 : input.getLimit()); + request.setDescending(input.getDescending() == null ? true : input.getDescending()); + try { + Result result = yunxinV2ApiServices.getMessageService().queryMessagesByPage(request); + if (result.isSuccess()) { + QueryMessagesByPageResponseV2 response = result.getResponse(); + // 获取成功 + log.info("===> getHistoryMessage success, response : {}, traceId : {}", JSONObject.toJSONString(response), result.getTraceId()); + List historyMessageOutputs = new ArrayList<>(); + response.getItems().forEach(item -> { + HistoryMessageOutput output = new HistoryMessageOutput(); + output.setFromUserId(imGenerator.decodeImAccountId(item.getSenderId())); + output.setToUserId(imGenerator.decodeImAccountId(item.getReceiverId())); + output.setContent(item.getText()); + output.setMessageType(MessageTypeEnum.getByCode(item.getMessageType())); + output.setAttachment(item.getAttachment()); + output.setExtension(item.getExtension()); + output.setCreateTime(item.getCreateTime()); + historyMessageOutputs.add(output); + }); + if(input.getDescending() != null && !input.getDescending()) { + //顺序翻转,末尾的排到第一 + Collections.reverse(historyMessageOutputs); + } + return historyMessageOutputs; + } else { + // 获取失败 + log.info("===> getHistoryMessage fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> getHistoryMessage error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + return Collections.emptyList(); + } + + @Override + public QueryMessageResponseV2 getAiSendMessage(Long aiId, Long userId, Long messageServerId) { + String fromAccountId = imGenerator.genImAccountId(aiId, ImUserTypeEnum.r); + String toAccountId = imGenerator.genImAccountId(userId, ImUserTypeEnum.u); + + QueryMessageRequestV2 request = new QueryMessageRequestV2(); + request.setConversationId(fromAccountId + "|1|" + toAccountId); + request.setMessageServerId(messageServerId); + try { + Result result = yunxinV2ApiServices.getMessageService().queryMessage(request); + if (result.isSuccess()) { + // 获取成功 + log.info("===> getAiSendMessage success, response : {}, traceId : {}", JSONObject.toJSONString(result.getResponse()), result.getTraceId()); + } else { + // 获取失败 + log.info("===> getAiSendMessage fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + return result.getResponse(); + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> getAiSendMessage error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + return null; + } + + @Override + public void updateAiSendCustomImageMessage(Long aiId, Long userId, Long messageServerId, AttachBo attachment) { + // 查询消息 + QueryMessageResponseV2 queryMessageResponseV2 = getAiSendMessage(aiId, userId, messageServerId); + if(queryMessageResponseV2 == null) { + return; + } + // 更新消息 + try { + String fromAccountId = imGenerator.genImAccountId(aiId, ImUserTypeEnum.r); + ModifyMessageRequestV2 request = new ModifyMessageRequestV2(); + request.setOperator(fromAccountId); + request.setType(1); + ModifyMessageRequestV2.Message message = new ModifyMessageRequestV2.Message(); + message.setMessageServerId(queryMessageResponseV2.getMessageServerId()); + message.setReceiverId(queryMessageResponseV2.getReceiverId()); + message.setTime(queryMessageResponseV2.getCreateTime()); + message.setSenderId(queryMessageResponseV2.getSenderId()); + message.setMessageType(queryMessageResponseV2.getMessageType()); + message.setText(queryMessageResponseV2.getText()); + request.setMessage(message); + //处理自定义字段 + Map attachmentMap = JSONObject.parseObject(JSONObject.toJSONString(attachment), Map.class); + message.setAttachment(attachmentMap); + Result result = yunxinV2ApiServices.getMessageService().modifyMessage(request); + if (result.isSuccess()) { + // 获取成功 + log.info("===> updateAiSendCustomImageMessage success, response : {}, traceId : {}", JSONObject.toJSONString(result.getResponse()), result.getTraceId()); + } else { + // 获取失败 + log.info("===> updateAiSendCustomImageMessage fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> updateAiSendCustomImageMessage error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + } + + @Override + public void updateAiSendMessage(Long aiId, Long userId, Long messageServerId, Map extension) { + // 查询消息 + QueryMessageResponseV2 queryMessageResponseV2 = getAiSendMessage(aiId, userId, messageServerId); + if(queryMessageResponseV2 == null) { + return; + } + // 更新消息 + try { + String fromAccountId = imGenerator.genImAccountId(aiId, ImUserTypeEnum.r); + ModifyMessageRequestV2 request = new ModifyMessageRequestV2(); + request.setOperator(fromAccountId); + request.setType(1); + ModifyMessageRequestV2.Message message = new ModifyMessageRequestV2.Message(); + message.setMessageServerId(queryMessageResponseV2.getMessageServerId()); + message.setReceiverId(queryMessageResponseV2.getReceiverId()); + message.setTime(queryMessageResponseV2.getCreateTime()); + message.setSenderId(queryMessageResponseV2.getSenderId()); + message.setMessageType(queryMessageResponseV2.getMessageType()); + message.setText(queryMessageResponseV2.getText()); + request.setMessage(message); + //处理扩展字段 + String queryExtension = queryMessageResponseV2.getExtension(); + if(StringUtils.isNotEmpty(queryExtension)) { + Map queryExtensionMap = JSONObject.parseObject(queryExtension, Map.class); + queryExtensionMap.putAll(extension); + request.setExtension(JSONObject.toJSONString(queryExtensionMap)); + } else { + request.setExtension(JSONObject.toJSONString(extension)); + } + Result result = yunxinV2ApiServices.getMessageService().modifyMessage(request); + if (result.isSuccess()) { + // 获取成功 + log.info("===> updateAiSendMessage success, response : {}, traceId : {}", JSONObject.toJSONString(result.getResponse()), result.getTraceId()); + } else { + // 获取失败 + log.info("===> updateAiSendMessage fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> updateAiSendMessage error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ImUserServiceImpl.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ImUserServiceImpl.java new file mode 100644 index 0000000..69d2de6 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ImUserServiceImpl.java @@ -0,0 +1,123 @@ +package com.sonic.pigeon.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.netease.nim.server.sdk.core.Result; +import com.netease.nim.server.sdk.core.exception.YunxinSdkException; +import com.netease.nim.server.sdk.im.v2.YunxinV2ApiServices; +import com.netease.nim.server.sdk.im.v2.account.request.CreateAccountRequestV2; +import com.netease.nim.server.sdk.im.v2.account.response.CreateAccountResponseV2; +import com.netease.nim.server.sdk.im.v2.users.request.GetUserRequestV2; +import com.netease.nim.server.sdk.im.v2.users.request.UpdateUserRequestV2; +import com.netease.nim.server.sdk.im.v2.users.response.GetUserResponseV2; +import com.netease.nim.server.sdk.im.v2.users.response.UpdateUserResponseV2; +import com.sonic.pigeon.domain.output.ImUserAccountOutput; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import com.sonic.pigeon.lib.input.CreateImUserInput; +import com.sonic.pigeon.lib.input.EditImUserInput; +import com.sonic.pigeon.service.ImUserService; +import com.sonic.pigeon.utils.ImGenerator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class ImUserServiceImpl implements ImUserService { + + @Autowired + private YunxinV2ApiServices yunxinV2ApiServices; + @Autowired + private ImGenerator imGenerator; + + @Override + public void createImUser(CreateImUserInput input) { + CreateAccountRequestV2 request = new CreateAccountRequestV2(); + //生成账号用户ID + request.setAccountId(imGenerator.genImAccountId(input.getUserId(), input.getImUserType())); + //生成账号登录密码 + request.setToken(imGenerator.genImAccountToken(request.getAccountId())); + + //创建用户基础信息 + CreateAccountRequestV2.UserInformation userInformation = new CreateAccountRequestV2.UserInformation(); + userInformation.setName(input.getNickname()); + userInformation.setAvatar(input.getHeadImage()); + userInformation.setGender(input.getGender()); + userInformation.setBirthday(input.getBirthday()); + userInformation.setExtension(input.getExtension() == null ? null : JSONObject.toJSONString(input.getExtension())); + + request.setUserInformation(userInformation); + + try { + Result result = yunxinV2ApiServices.getAccountService().createAccount(request); + if (result.isSuccess()) { + CreateAccountResponseV2 response = result.getResponse(); + // 注册成功 + log.info("===> register success, response : {}, traceId : {}", JSONObject.toJSONString(response), result.getTraceId()); + } else { + // 注册失败,如参数错误、重复注册等 + log.info("===> register fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> register error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + } + + @Override + public void editImUser(EditImUserInput input) { + UpdateUserRequestV2 request = new UpdateUserRequestV2(); + //生成账号用户ID + request.setAccountId(imGenerator.genImAccountId(input.getUserId(), input.getImUserType())); + request.setName(input.getNickname()); + request.setAvatar(input.getHeadImage()); + request.setGender(input.getGender()); + request.setBirthday(input.getBirthday()); + request.setExtension(input.getExtension() == null ? null : JSONObject.toJSONString(input.getExtension())); + try { + Result result = yunxinV2ApiServices.getUserService().updateUser(request); + if (result.isSuccess()) { + UpdateUserResponseV2 response = result.getResponse(); + // 更新成功 + log.info("===> update success, response : {}, traceId : {}", JSONObject.toJSONString(response), result.getTraceId()); + } else { + // 更新失败,如参数错误等 + log.info("===> update fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> update error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + } + + @Override + public GetUserResponseV2 getImUser(Long userId, ImUserTypeEnum imUserType) { + GetUserRequestV2 request = new GetUserRequestV2(); + //生成账号用户ID + request.setAccountId(imGenerator.genImAccountId(userId, imUserType)); + try { + Result result = yunxinV2ApiServices.getUserService().getUser(request); + GetUserResponseV2 response = result.getResponse(); + if (result.isSuccess()) { + // 更新成功 + log.info("===> getImUser success, response : {}, traceId : {}", JSONObject.toJSONString(response), result.getTraceId()); + } else { + // 更新失败,如参数错误等 + log.info("===> getImUser fail, code : {}, msg : {}, traceId : {}", result.getCode(), result.getMsg(), result.getTraceId()); + } + return response; + } catch (YunxinSdkException e) { + // 超时等异常 + log.error("===> getImUser error, endpoint : {}, traceId : {}", e.getContext().getEndpoint(), e.getTraceId()); + } + return null; + } + + @Override + public ImUserAccountOutput getImUserAccount(Long userId) { + ImUserAccountOutput output = new ImUserAccountOutput(); + output.setAccountId(imGenerator.genImAccountId(userId, ImUserTypeEnum.u)); + output.setToken(imGenerator.genImAccountToken(output.getAccountId())); + return output; + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/MessageServiceImpl.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/MessageServiceImpl.java new file mode 100644 index 0000000..5b6e1bc --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/MessageServiceImpl.java @@ -0,0 +1,77 @@ +package com.sonic.pigeon.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.sonic.common.rpc.Page; +import com.sonic.daosupport.converter.PageConverter; +import com.sonic.pigeon.dao.MessageDao; +import com.sonic.pigeon.domain.entity.Message; +import com.sonic.pigeon.domain.input.MessageListInput; +import com.sonic.pigeon.domain.input.SendMessageInput; +import com.sonic.pigeon.domain.output.MessageListOutput; +import com.sonic.pigeon.enums.MessageStatusEnum; +import com.sonic.pigeon.service.MessageService; +import com.sonic.pigeon.service.MessageStatService; +import com.sonic.pigeon.utils.BeanConver; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class MessageServiceImpl extends ServiceImpl implements MessageService { + + @Autowired + private MessageStatService messageStatService; + + @Override + public void sendMessage(List reqList) { + reqList.forEach(req -> { + Message message = BeanConver.copeBean(req, Message.class); + message.setStatus(MessageStatusEnum.NO_READ.getIndex()); + message.setIsDelete(false); + message.setCreateTime(LocalDateTime.now()); + message.setEditTime(LocalDateTime.now()); + //保存 + save(message); + //更新统计表未读数,最近未读消息内容及时间 + messageStatService.updateUnread(req.getUserId(), 1, req.getContent()); + }); + } + + @Override + public Page messageList(Long userId, MessageListInput input) { + //分页获取 + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(input.getPage().getPn(), input.getPage().getPs()); + IPage messagePage = page(page, Wrappers.lambdaQuery() + .eq(Message::getUserId, userId) + .eq(Message::getIsDelete, false) + .orderByDesc(Message::getId) + ); + List records = messagePage.getRecords(); + //没有数据直接返回 + if (CollectionUtils.isEmpty(records)) { + return new Page(); + } + //未读消息列表 + List unreadIdList = records.stream().filter(e->e.getStatus() == 0).map(Message::getId).collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(unreadIdList)) { + //更新消息为已读 + update(Wrappers.lambdaUpdate().set(Message::getStatus, MessageStatusEnum.READ.getIndex()).in(Message::getId, unreadIdList)); + //统计表未读数要减去本次拉取的数量 + messageStatService.updateUnread(userId, -unreadIdList.size(), null); + } + return PageConverter.convert(messagePage, message -> { + return BeanConver.copeBean(message, MessageListOutput.class); + }); + } +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/MessageStatServiceImpl.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/MessageStatServiceImpl.java new file mode 100644 index 0000000..00e709f --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/MessageStatServiceImpl.java @@ -0,0 +1,60 @@ +package com.sonic.pigeon.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.common.utils.RedisLock; +import com.sonic.pigeon.dao.MessageStatDao; +import com.sonic.pigeon.domain.entity.MessageStat; +import com.sonic.pigeon.domain.output.MessageStatOutput; +import com.sonic.pigeon.service.MessageStatService; +import com.sonic.pigeon.utils.BeanConver; +import com.sonic.pigeon.utils.RedisKeyUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +public class MessageStatServiceImpl extends ServiceImpl implements MessageStatService { + + @Autowired + private MessageStatDao messageStatDao; + @Autowired + private RedisLock.RedisWrapper redisWrapper; + @Autowired + private RedisKeyUtils redisKeyUtils; + + + @Override + public MessageStatOutput messageStat(Long userId) { + MessageStat messageStat = getOne(Wrappers.lambdaQuery().eq(MessageStat::getUserId, userId).last("limit 1")); + MessageStatOutput output = new MessageStatOutput(); + if (messageStat != null) { + output = BeanConver.copeBean(messageStat, MessageStatOutput.class); + } + return output; + } + + @Override + public void updateUnread(Long userId, int unreadNum, String latestContent) { + //redis键 + String messageStatLockKey = redisKeyUtils.messageStatLockKey(userId); + //加锁处理 防止并发问题 + RedisLock redisLock = new RedisLock(messageStatLockKey, redisWrapper); + redisLock.tryAcquireRun(() -> { + //获取是否有对应用户的消息统计 + MessageStat messageStat = getOne(Wrappers.lambdaQuery().eq(MessageStat::getUserId, userId).last("limit 1")); + if (messageStat == null) { + //没有就初始化一个 + messageStat = new MessageStat(); + messageStat.setUserId(userId); + messageStat.setCreateTime(LocalDateTime.now()); + messageStat.setEditTime(LocalDateTime.now()); + save(messageStat); + } + //更新未读消息数,最近未读消息内容及时间 + messageStatDao.updateUnread(userId, unreadNum, latestContent); + return true; + }); + } +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ReceiveImMsgServiceImpl.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ReceiveImMsgServiceImpl.java new file mode 100644 index 0000000..bcb90da --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/service/impl/ReceiveImMsgServiceImpl.java @@ -0,0 +1,153 @@ +package com.sonic.pigeon.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.sonic.lion.lib.client.PayClient; +import com.sonic.lion.lib.output.AccountBuffOutput; +import com.sonic.pigeon.domain.bo.ImUserBo; +import com.sonic.pigeon.domain.input.ReceiveImMsgInput; +import com.sonic.pigeon.lib.bo.AttachBo; +import com.sonic.pigeon.lib.client.UserDeductionClient; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import com.sonic.pigeon.lib.enums.MessageTypeEnum; +import com.sonic.pigeon.service.CommonSendMqService; +import com.sonic.pigeon.service.ReceiveImMsgService; +import com.sonic.pigeon.utils.ImGenerator; +import com.sonic.pigeon.utils.RedisKeyUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * @Author zzhan + * @Date 2020/10/28 11:19 + * @Version 1.0 + */ +@Slf4j +@Service +public class ReceiveImMsgServiceImpl implements ReceiveImMsgService { + + @Autowired + private ImGenerator imGenerator; + @Autowired + private PayClient payClient; + @Autowired + private UserDeductionClient userDeductionClient; + + + private List DEFAULT_CHECK_MSG_TYPE = new ArrayList<>(); + + /** + * 一条文本预扣金额 + */ + private final static Long TEXT_DEDUCTION_AMOUNT = 100L; + @Autowired + private CommonSendMqService commonSendMqService; + @Autowired + private RedisKeyUtils redisKeyUtils; + @Autowired + private StringRedisTemplate stringRedisTemplate; + + + @PostConstruct + public void init() { + DEFAULT_CHECK_MSG_TYPE.add("TEXT"); + DEFAULT_CHECK_MSG_TYPE.add("PICTURE"); + DEFAULT_CHECK_MSG_TYPE.add("IMAGE"); + } + + @Override + public Object receiveImMsgCheck(ReceiveImMsgInput input) { + //处理逻辑,判断发送人。接收人 + ImUserBo fromUserBo = imGenerator.getImUserBo(input.getFromAccount()); + ImUserBo toUserBo = imGenerator.getImUserBo(input.getTo()); + Long fromUserId = fromUserBo.getUserId(); + //判断图片消息的发送 + if(ImUserTypeEnum.u == fromUserBo.getImUserType() && ImUserTypeEnum.r == toUserBo.getImUserType()) { + //解析出自定义消息内容体 + AttachBo attachBo = StringUtils.isEmpty(input.getAttach()) ? null : JSONObject.parseObject(input.getAttach(), AttachBo.class); + if(MessageTypeEnum.CUSTOM.name().equalsIgnoreCase(input.getMsgType()) && attachBo != null && "IMAGE".equalsIgnoreCase(attachBo.getType())) { + String level = stringRedisTemplate.opsForValue().get(redisKeyUtils.aiUserHeartbeatLevelCacheKey(fromUserId, toUserBo.getUserId())); + if(StringUtils.isEmpty(level) || "LEVEL_1".equals(level)) { + return buildFailResult("20001", "不允许发送图片消息!"); + } + } + } + + //余额不足,返回20000错误码 + if (!isBalanceNotCheck(input) && checkBalanceIsInsufficient(fromUserId)) { + //发送MQ,计算费用结算 + commonSendMqService.userBalanceInsufficientCheckoutMq(fromUserId); + return buildFailResult("20000", "你的余额不足!"); + } + return buildSuccessResult(); + } + + /** + * 是否不要余额检测 + * + * @param input + * @return + */ + private boolean isBalanceNotCheck(ReceiveImMsgInput input) { + String msgType = input.getMsgType(); + String attach = input.getAttach(); + JSONObject attachJson = JSONObject.parseObject(attach); + //自定义消息且是语音通话 + if ("CUSTOM".equals(msgType) && "CALL".equals(attachJson.getString("type"))) { + return true; + } + return false; + } + + /** + * 检测余额是否不足 + * + * @param userId + * @return + */ + private boolean checkBalanceIsInsufficient(Long userId) { + try { + AccountBuffOutput accountBuff = payClient.getAccountBuff(userId); + Long balance = accountBuff != null && accountBuff.getBalance() > 0 ? accountBuff.getBalance() : 0; + log.info("checkBalanceIsInsufficient balance:{},totalDeductionAmount:{}", balance, TEXT_DEDUCTION_AMOUNT); + if (balance < TEXT_DEDUCTION_AMOUNT) { + return true; + } + } catch (Exception e) { + log.error("checkBalanceIsInsufficient error:", e); + } + return false; + } + + public Map buildSuccessResult() { + return buildResult("0", "", ""); + } + + private Map buildFailResult(String responseCode, String callbackExt) { + return buildResult("1", responseCode, callbackExt); + } + + private Map buildResult(String errorCode, String responseCode, String callbackExt) { + Map result = new HashMap<>(4); + //0:表示回调通过,允许执行。 + //1:表示回调不通过,取消执行。如果设置了合法的自定义错误码(responseCode),则发送端会收到自定义错误码,否则发送端会收到403错误码。 + result.put("errCode", errorCode); + //1、当errCode=1时有效 + //2、范围是20000-20099,其他值无效,会被忽略 + //3、特别的,对于消息类型的第三方回调(eventType=1、2、6、22),支持设置为200的错误码,客户端表现为消息发送成功,其实消息发送失败 + result.put("responseCode", responseCode); + result.put("modifyResponse", ""); + result.put("callbackExt", callbackExt); + return result; + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/BeanConver.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/BeanConver.java new file mode 100644 index 0000000..06ce68b --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/BeanConver.java @@ -0,0 +1,64 @@ +package com.sonic.pigeon.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cglib.beans.BeanCopier; + +import java.util.ArrayList; +import java.util.List; + +/** + * 对象拷贝 + * @author dev-center + */ +public class BeanConver { + private final static Logger LOG = LoggerFactory.getLogger(BeanConver.class); + + /** + * 实例类转换 + * + * @param source 源对象 + * @param target 目标对象 + * @param 源对象 + * @param 目标对象 + */ + public static K copeBean(T source, Class target) { + if (source == null) { + return null; + } + BeanCopier beanCopier = BeanCopier.create(source.getClass(), target, false); + K result = null; + try { + result = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(source, result, null); + return result; + } + + /** + * page实例类转换 + * + * @param 源对象 + * @param 目标对象 + * @param source 源对象 + * @param target 目标对象 + * @return + */ + public static List copeList(List source, Class target) { + List list = new ArrayList<>(); + for (T af : source) { + BeanCopier beanCopier = BeanCopier.create(af.getClass(), target, false); + K af1 = null; + try { + af1 = (K) target.newInstance(); + } catch (Exception e) { + LOG.error("实例转换出错"); + } + beanCopier.copy(af, af1, null); + list.add(af1); + } + return list; + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/CheckSumBuilder.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/CheckSumBuilder.java new file mode 100644 index 0000000..2bf3b47 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/CheckSumBuilder.java @@ -0,0 +1,54 @@ +package com.sonic.pigeon.utils; + +import java.security.MessageDigest; + +public class CheckSumBuilder { + + /** + * 计算并获取CheckSum + * @param appSecret + * @param nonce + * @param curTime + * @return + */ + public static String getCheckSum(String appSecret, String nonce, String curTime) { + return encode("sha1", appSecret + nonce + curTime); + } + + /** + * 计算并获取md5值 + * @param requestBody + * @return + */ + public static String getMD5(String requestBody) { + return encode("md5", requestBody); + } + + private static String encode(String algorithm, String value) { + if (value == null) { + return null; + } + try { + MessageDigest messageDigest + = MessageDigest.getInstance(algorithm); + messageDigest.update(value.getBytes()); + return getFormattedText(messageDigest.digest()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String getFormattedText(byte[] bytes) { + int len = bytes.length; + StringBuilder buf = new StringBuilder(len * 2); + for (int j = 0; j < len; j++) { + buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]); + buf.append(HEX_DIGITS[bytes[j] & 0x0f]); + } + return buf.toString(); + } + + private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/HexUtils.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/HexUtils.java new file mode 100644 index 0000000..678860b --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/HexUtils.java @@ -0,0 +1,69 @@ +package com.sonic.pigeon.utils; + +public class HexUtils { + private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + public static char[] encodeHex(byte[] data) { + return encodeHex(data, true); + } + + public static char[] encodeHex(byte[] data, boolean toLowerCase) { + return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + public static String encodeHexStr(byte[] data) { + return encodeHexStr(data, true); + } + + public static String encodeHexStr(byte[] data, boolean toLowerCase) { + return encodeHexStr(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + public static byte[] decodeHex(String data) { + return decodeHex(data.toCharArray()); + } + + public static byte[] decodeHex(char[] data) { + int len = data.length; + if ((len & 0x01) != 0) { + throw new RuntimeException("Unknown char"); + } + + byte[] out = new byte[len >> 1]; + for (int i = 0, j = 0; j < len; i++) { + int f = toDigit(data[j], j) << 4; + j++; + f = f | toDigit(data[j], j); + j++; + out[i] = (byte) (f & 0xFF); + } + + return out; + } + + private static char[] encodeHex(byte[] data, char[] toDigits) { + int l = data.length; + char[] out = new char[l << 1]; + for (int i = 0, j = 0; i < l; i++) { + out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; + out[j++] = toDigits[0x0F & data[i]]; + } + return out; + } + + private static String encodeHexStr(byte[] data, char[] toDigits) { + return new String(encodeHex(data, toDigits)); + } + + private static int toDigit(char ch, int index) { + int digit = Character.digit(ch, 16); + if (digit == -1) { + throw new RuntimeException("Invalid hex char " + ch + ", index at " + index); + } + return digit; + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/HttpUtil.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/HttpUtil.java new file mode 100644 index 0000000..307f9d1 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/HttpUtil.java @@ -0,0 +1,74 @@ +package com.sonic.pigeon.utils; + +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +/** + * Http + **/ +public class HttpUtil { + + /** + * 获取body内容 + * + * @param request + * @return + * @throws Exception + */ + public static String getBody(HttpServletRequest request) throws Exception { + String body; + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader bufferedReader = null; + try { + InputStream inputStream = request.getInputStream(); + if (inputStream != null) { + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + char[] charBuffer = new char[128]; + int bytesRead = -1; + while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { + stringBuilder.append(charBuffer, 0, bytesRead); + } + } else { + stringBuilder.append(""); + } + } catch (IOException ex) { + throw ex; + } finally { + if (bufferedReader != null) { + try { + bufferedReader.close(); + } catch (IOException ex) { + throw ex; + } + } + } + body = stringBuilder.toString(); + return body; + } + + + /** + * 获取请求头信息 + * + * @param request + * @return + */ + public static Map getHeadersInfo(HttpServletRequest request) { + Map map = new HashMap(); + @SuppressWarnings("rawtypes") + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String key = (String) headerNames.nextElement(); + String value = request.getHeader(key); + map.put(key, value); + } + return map; + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/ImGenerator.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/ImGenerator.java new file mode 100644 index 0000000..5c4626e --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/ImGenerator.java @@ -0,0 +1,90 @@ +package com.sonic.pigeon.utils; + +import com.sonic.common.enums.AppEnv; +import com.sonic.pigeon.domain.bo.ImUserBo; +import com.sonic.pigeon.lib.enums.ImUserTypeEnum; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @author code + */ +@Component +public class ImGenerator { + + @Value("${spring.profiles.active}") + private String runMode; + + /** + * 分隔符 + */ + private static final String TEMPLATE = "@"; + + /** + * 测试环境 + */ + private static final String TEMPLATE_TEST = "t"; + + + /** + * 生成IM的账号ID + * @param userId + * @param userType + * @return + */ + public String genImAccountId(Long userId, ImUserTypeEnum userType) { + StringBuilder accountId = new StringBuilder(); + accountId.append(userId); + if (AppEnv.product.name().equals(runMode)) { + //eg: 10001#U + accountId.append(TEMPLATE).append(userType); + } else { + //eg: 10001#U#T (第三位代表环境) + accountId.append(TEMPLATE).append(userType).append(TEMPLATE).append(TEMPLATE_TEST); + } + return accountId.toString(); + } + + /** + * 获取IM用户信息 + * @param accountId + * @return + */ + public ImUserBo getImUserBo(String accountId) { + ImUserBo imUserBo = new ImUserBo(); + imUserBo.setUserId(decodeImAccountId(accountId)); + //获取用户类型 + imUserBo.setImUserType(decodeImUserType(accountId)); + return imUserBo; + } + + /** + * 解密IM账号ID + * @param accountId + * @return + */ + public Long decodeImAccountId(String accountId) { + return Long.valueOf(accountId.substring(0, accountId.indexOf(TEMPLATE))); + } + + /** + * 解析用户类型 + * @param accountId + * @return + */ + public ImUserTypeEnum decodeImUserType(String accountId) { + accountId = accountId.replace(TEMPLATE + TEMPLATE_TEST, ""); + String userTypeStr = accountId.substring(accountId.indexOf(TEMPLATE) + 1); + return ImUserTypeEnum.getImUserType(userTypeStr); + } + + /** + * 获取访问IM的Token + * @param accountId + * @return + */ + public String genImAccountToken(String accountId) { + return new Md5Hash(UserSaltGenerator.generateSaltByUserId(accountId) + accountId).toHex(); + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/LimitUtils.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/LimitUtils.java new file mode 100644 index 0000000..16c7260 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/LimitUtils.java @@ -0,0 +1,60 @@ +package com.sonic.pigeon.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 限流工具类 + */ +@Slf4j +@Component +public class LimitUtils { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * redis key不存在的过期时间值 + */ + private Integer NULL_KEY_EXP_TIME_VALUE = -2; + + /** + * redis key永不过期的过期时间值 + */ + private Integer NEVER_EXPIRES_VALUE = -1; + + /** + * 根据请求参数进行限流检查 + * @param redisKey 自定义封装的redisKey + * @param count 限流的数量 + * @param time 时间段:单位为秒 + */ + public boolean defaultLimitCheckByKey(String redisKey, int count, int time) { + //用户操作某个话题的点赞,超过4次就提示,锁定5s + int num; + Long expTime = stringRedisTemplate.getExpire(redisKey); + if (expTime == null || expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", time, TimeUnit.SECONDS); + } else { + num = stringRedisTemplate.opsForValue().increment(redisKey, 1).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime != null && expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis并返回false + stringRedisTemplate.delete(redisKey); + return false; + } + if (num > count) { + log.info("===>超过了限定的次数[" + count + "]"); + return true; + } + return false; + } + + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/Md5Hash.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/Md5Hash.java new file mode 100644 index 0000000..cb6faf6 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/Md5Hash.java @@ -0,0 +1,43 @@ +package com.sonic.pigeon.utils; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * MD5加密工具类 + */ +public class Md5Hash { + + private final String input; + + public Md5Hash(String input) { + this.input = input; + } + + /** + * 将输入字符串进行MD5加密并返回十六进制字符串 + * @return MD5加密后的十六进制字符串 + */ + public String toHex() { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(input.getBytes()); + return bytesToHex(digest); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 algorithm not available", e); + } + } + + /** + * 将字节数组转换为十六进制字符串 + * @param bytes 字节数组 + * @return 十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/RedisKeyUtils.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/RedisKeyUtils.java new file mode 100644 index 0000000..a8b94fe --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/RedisKeyUtils.java @@ -0,0 +1,66 @@ +package com.sonic.pigeon.utils; + +import com.sonic.common.AppRuntime; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * 统一的 redis key 管理工具类(方便后续的维护和查找) + * + * @Author code + * @Date 2021/9/24 + * @Version 1.0 + */ +@Slf4j +@Service +public class RedisKeyUtils { + + @Value("${spring.profiles.active}") + private String runMode; + @Autowired + private AppRuntime appRuntime; + + /** + * 反馈上锁 + * + * @return + */ + public String feedbackLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "feedback", userId); + } + + /** + * 反馈限流 + * + * @return + */ + public String feedbackLimitKey(Long userId) { + return appRuntime.buildPrefixKey("limit", "feedbackV2", userId); + } + + /** + * 消息统计锁 + * + * @return + */ + public String messageStatLockKey(Long userId) { + return appRuntime.buildPrefixKey("lock", "messageStat", userId); + } + + + /** + * 关系等级缓存 + * @param userId + * @param aiId + * @return + */ + public String aiUserHeartbeatLevelCacheKey(Long userId, Long aiId) { + if("test".equalsIgnoreCase(runMode)) { + return "1002:test:cache:heartbeatLevel:" + userId + ":" + aiId; + } + return "1002:cache:heartbeatLevel:" + userId + ":" + aiId; + } + +} diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/UUIDGenerator.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/UUIDGenerator.java new file mode 100644 index 0000000..7d749a7 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/UUIDGenerator.java @@ -0,0 +1,100 @@ +package com.sonic.pigeon.utils; + +import java.util.Random; +import java.util.UUID; + +public class UUIDGenerator { + + private static final String SPLITOR = "-"; + private static final String BLANK = ""; + + public static String BASE_STRING = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + public static String[] BASE_CHARS = new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", + "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", + "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", + "T", "U", "V", "W", "X", "Y", "Z"}; + + /** UUID 32位 */ + public static String generateLongUuid() { + return generateLongUuid(true); + } + + /** UUID 32位 */ + public static String generateLongUuid(boolean isUpperCase) { + String uuid = UUID.randomUUID().toString().replace(SPLITOR, BLANK); + if (isUpperCase) { + return uuid.toUpperCase(); + } + return uuid.toLowerCase(); + } + + /** UUID转为8位 */ + public static String generateShortUuid() { + StringBuilder shortBuffer = new StringBuilder(8); + String uuid = UUID.randomUUID().toString().replace(SPLITOR, BLANK); + for (int i = 0; i < 8; i++) { + String str = uuid.substring(i * 4, i * 4 + 4); + int x = Integer.parseInt(str, 16); + shortBuffer.append(BASE_CHARS[x % 0x3E]); + } + return shortBuffer.toString(); + } + + public static String generate16Uuid() { + StringBuilder shortBuffer = new StringBuilder(16); + String uuid = UUID.randomUUID().toString().replace(SPLITOR, BLANK); + for (int i = 0; i < 16; i++) { + int start = i * 2; + int end = Math.min(i * 2 + 4, 32); + String str = uuid.substring(start, end); + int x = Integer.parseInt(str, 16); + shortBuffer.append(BASE_CHARS[x % 0x3E]); + } + return shortBuffer.toString(); + } + + /** + * 生成32位的token + * @return + */ + public static String generate32Uuid() { + return generate16Uuid() + generate16Uuid(); + } + + /** + * 生成48位的token + * @return + */ + public static String generate48Uuid() { + return generate16Uuid() + generate16Uuid() + generate16Uuid(); + } + + /** + * 生成64位的token + * @return + */ + public static String generate64Uuid() { + return generate16Uuid() + generate16Uuid() + generate16Uuid() + generate16Uuid(); + } + + /** length表示生成字符串的长度 */ + public static String getRandomString(int length) { + Random random = new Random(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + int number = random.nextInt(BASE_STRING.length()); + sb.append(BASE_STRING.charAt(number)); + } + return sb.toString(); + } + + public static void main(String[] args) { + long start = System.currentTimeMillis(); + for (int i = 0; i < 30000; i++) { + System.out.println("D" + generateShortUuid() + getRandomString(0)); + } + System.out.println(System.currentTimeMillis() - start); + } + +} \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/UserSaltGenerator.java b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/UserSaltGenerator.java new file mode 100644 index 0000000..9f3cfe2 --- /dev/null +++ b/sonic-pigeon/server/src/main/java/com/sonic/pigeon/utils/UserSaltGenerator.java @@ -0,0 +1,45 @@ +package com.sonic.pigeon.utils; + +/** + * 基于用户ID生成动态盐值的工具类 + */ +public class UserSaltGenerator { + + private static final String BASE_SALT = "LMiopk7%^TUGhRCUYV089ioHI87gBF76"; + + /** + * 根据用户ID生成动态盐值 + * @param accountId 用户ID + * @return 基于用户ID生成的盐值 + */ + public static String generateSaltByUserId(String accountId) { + if (accountId == null) { + throw new IllegalArgumentException("用户ID不能为空"); + } + + // 将用户ID与基础盐值结合,然后进行位运算和字符串操作生成动态盐值 + String userIdStr = accountId; + int userIdHash = userIdStr.hashCode(); + + // 使用用户ID的哈希值和基础盐值生成动态盐值 + StringBuilder dynamicSalt = new StringBuilder(); + + // 取用户ID哈希值的某些位作为偏移量 + int offset = Math.abs(userIdHash) % BASE_SALT.length(); + + // 循环处理基础盐值,生成动态盐值 + for (int i = 0; i < BASE_SALT.length(); i++) { + int index = (i + offset) % BASE_SALT.length(); + char saltChar = BASE_SALT.charAt(index); + + // 结合用户ID的数字特征进一步混淆 + int userIdDigit = Character.getNumericValue(userIdStr.charAt(i % userIdStr.length())); + char mixedChar = (char) (saltChar ^ (userIdDigit + i) % 256); + + dynamicSalt.append(mixedChar); + } + + return dynamicSalt.toString(); + } + +} diff --git a/sonic-pigeon/server/src/main/resources/application-dev.yml b/sonic-pigeon/server/src/main/resources/application-dev.yml new file mode 100644 index 0000000..45c70d0 --- /dev/null +++ b/sonic-pigeon/server/src/main/resources/application-dev.yml @@ -0,0 +1,39 @@ +spring: + datasource: + # TODO: 需要改写为Dev环境的Mysql地址, 数据库名称,用户名和密码 + url: jdbc:mysql://54.223.196.180:3306/sonic-bear?autoReconnect=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull + username: root + password: toukagames1234 + + redis: + # TODO: 需要改写为DEV环境的Redis地址, 数据库名称和密码 + host: 54.223.196.180 + port: 6379 + database: 0 + password: 123456 + # cluster: + # nodes: 192.168.100.238 + # ssl: + # enabled: true + + rabbitmq: + # TODO: 需要改写为dev环境的rabbitmq地址, 用户名和密码 + host: 54.223.196.180 + port: 5672 + username: guest + password: toukagames1234 + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +im: + appKey: xxx + appSecret: xxx + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bear.controller" \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/resources/application-local.yml b/sonic-pigeon/server/src/main/resources/application-local.yml new file mode 100644 index 0000000..60a8346 --- /dev/null +++ b/sonic-pigeon/server/src/main/resources/application-local.yml @@ -0,0 +1,39 @@ +spring: + datasource: + # TODO: 需要改写为Dev环境的Mysql地址, 数据库名称,用户名和密码 + url: jdbc:mysql://192.168.100.238:3306/sonic-bear?autoReconnect=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull + username: egirl_dev + password: lpkq609oI9eRc + + redis: + # TODO: 需要改写为DEV环境的Redis地址, 数据库名称和密码 + host: 192.168.100.238 + port: 6379 + database: 0 + password: Epal@2020 +# cluster: +# nodes: 192.168.100.238 +# ssl: +# enabled: true + + rabbitmq: + # TODO: 需要改写为dev环境的rabbitmq地址, 用户名和密码 + host: 192.168.100.238 + port: 5672 + username: guest + password: epal@2020 + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +im: + appKey: xxx + appSecret: xxx + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bear.controller" \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/resources/application-product.yml b/sonic-pigeon/server/src/main/resources/application-product.yml new file mode 100644 index 0000000..51ecca7 --- /dev/null +++ b/sonic-pigeon/server/src/main/resources/application-product.yml @@ -0,0 +1,38 @@ +spring: + datasource: + url: ${DB.MASTER.PIGEON.URL} + username: ${DB.MASTER.USERNAME} + password: ${DB.MASTER.PASSWORD} + + redis: +# host: ${REDIS.MAIN.HOST} +# port: ${REDIS.MAIN.PORT} +# database: 0 +# password: ${REDIS.MAIN.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + + rabbitmq: + host: ${MQ.HOST} + port: ${MQ.PORT} + username: ${MQ.USERNAME} + password: ${MQ.PASSWORD} + ssl: + enabled: true + +web: + frequency-alert: + # TODO 修改接口访问频率告警配置。 + # 默认配置含义为: /* 所有接口5秒访问500次. + rules: + - /*:5/500 + +im: + appKey: ${IM.APP_KEY} + appSecret: ${IM.APP_SECRET} + +#swagger展示相关的配置 +swagger: + enabled: false + base: + package: "com.sonic.bear.controller" diff --git a/sonic-pigeon/server/src/main/resources/application-test.yml b/sonic-pigeon/server/src/main/resources/application-test.yml new file mode 100644 index 0000000..91b077c --- /dev/null +++ b/sonic-pigeon/server/src/main/resources/application-test.yml @@ -0,0 +1,36 @@ +spring: + datasource: + url: ${DB.MASTER.PIGEON.URL} + username: ${DB.MASTER.USERNAME} + password: ${DB.MASTER.PASSWORD} + + redis: +# host: ${REDIS.MAIN.HOST} +# port: ${REDIS.MAIN.PORT} +# database: 0 +# password: ${REDIS.MAIN.PASSWORD} + cluster: + nodes: ${REDIS.CLUSTER.NODES} + + rabbitmq: + host: ${MQ.HOST} + port: ${MQ.PORT} + username: ${MQ.USERNAME} + password: ${MQ.PASSWORD} + ssl: + enabled: true + +#配置开启sql的执行日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +im: + appKey: ${IM.APP_KEY} + appSecret: ${IM.APP_SECRET} + +#swagger展示相关的配置 +swagger: + enabled: true + base: + package: "com.sonic.bear.controller" \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/resources/application.yml b/sonic-pigeon/server/src/main/resources/application.yml new file mode 100644 index 0000000..2645daf --- /dev/null +++ b/sonic-pigeon/server/src/main/resources/application.yml @@ -0,0 +1,86 @@ +spring: + profiles: + # profile目前支持以下5种:local/unittest/dev/test/product + # 开发的时候一般使用dev或者local + # 在测试环境/生产环境,该配置不起作用,会被外部传入的jvm启动参数(spring.profiles.active)或者环境变量覆盖 + active: dev + application: + # TODO: 更换项目名称 + name: pigeon + # 必须使用引号,否则会转成8进制 + id: 1000 + task: + execution: + pool: + max-size: 50 + core-size: 4 + queue-capacity: 20480 + keep-alive: 30s + # TODO: 如果不需要mysql,请移除datasource相关的所有配置 + datasource: + driver-class-name: com.mysql.jdbc.Driver + hikari: + auto-commit: true + connection-timeout: 20000 + maximum-pool-size: 30 + minimum-idle: 5 + idle-timeout: 300000 + max-lifetime: 1200000 + # TODO: 如果不需要Redis,请移除redis相关的所有配置 + redis: + lettuce: + pool: + max-active: 1000 + max-wait: 1000 + max-idle: 100 + + rabbitmq: + listener: + simple: + acknowledge-mode: manual + concurrency: 2 + max-concurrency: 10 + #限流 + prefetch: 1 + +# TODO: 如果不需要mysql,请移除mybatis-plus相关的所有配置 +mybatis-plus: + # 定义mybatis映射文件的位置 + mapper-locations: classpath:/mapper/*Mapper.xml + +web: + frequency-alert: + # TODO 修改接口访问频率告警配置, 如果不配置的FrequencyAlertInterceptor不会生效. + # 以下配置含义为: /* 1秒访问20次, /index 10秒访问100次. 访问频率超过该规则会触发告警consumer + rules: + - /*:1/20 + - /index:10/100 + +mq: + # 如无必要,无须修改 exchange + exchange: message-server-exchange + default: + # TODO: {Event.BuildInScene.code}-{appName}-queue + queue: bs-bear-queue + # TODO: {Event.BuildInScene.code}-{appName}-routing-key + routing-key: bs-bear-routing-key + #AI聊天的队列 + aiChat: + queue: ai-chat-queue + routing-key: ai-chat-routing-key + aiChatToFrog: + queue: ai-chatToFrog-queue + routing-key: ai-chatToFrog-routing-key + #文本,语音,语音通话预扣款,如果余额不足,发起扣款 + user-balance-insufficient-checkout: + queue: user-balance-insufficient-checkout-queue + routing-key: user-balance-insufficient-checkout-routing-key + +# swagger 默认开启,在生产环境关闭,节省资源 +swagger: + enabled: true + +# 禁用健康检查 +management: + endpoints: + enabled-by-default: false #关闭监控 diff --git a/sonic-pigeon/server/src/main/resources/logback-spring.xml b/sonic-pigeon/server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b8dd4d1 --- /dev/null +++ b/sonic-pigeon/server/src/main/resources/logback-spring.xml @@ -0,0 +1,62 @@ + + + + + + + + + + ${DefaultPattern} + + + + + + + + ${ColorfulPattern} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sonic-pigeon/server/src/main/resources/mapper/MessageMapper.xml b/sonic-pigeon/server/src/main/resources/mapper/MessageMapper.xml new file mode 100644 index 0000000..997015e --- /dev/null +++ b/sonic-pigeon/server/src/main/resources/mapper/MessageMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/resources/mapper/MessageStatMapper.xml b/sonic-pigeon/server/src/main/resources/mapper/MessageStatMapper.xml new file mode 100644 index 0000000..86bfb07 --- /dev/null +++ b/sonic-pigeon/server/src/main/resources/mapper/MessageStatMapper.xml @@ -0,0 +1,13 @@ + + + + + + update t_message_stat + set un_read = if((un_read + #{unreadNum})>0,un_read + #{unreadNum},0) + + ,latest_content = #{latestContent},latest_time = now() + + where user_id = #{userId} + + \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/resources/messages.properties b/sonic-pigeon/server/src/main/resources/messages.properties new file mode 100644 index 0000000..24cec7c --- /dev/null +++ b/sonic-pigeon/server/src/main/resources/messages.properties @@ -0,0 +1,20 @@ +NO_LOGIN=User is not logged in +USER_NOT_EXIST=User does not exist +APP_CLIENT_NOT_EXIST=Login client does not exist +APP_CLIENT_NOT_ALLOW=Not authorized to log in to the client +SESSION_EXPIRED=Session is expired +SESSION_INVALID=Session is invalid +PASSWORD_INVALID=Invalid account or password +AUTH_FAIL=Authorization failed +USER_FROZEN=User is frozen +SYS_PARAMETERS_VALIDATE_EXCEPTION=Parameter validation error +SYS_VALIDATION_FAILED_ERROR=Validation failed, invalid request +ACCOUNT_NOT_REGISTER=Incorrect username or password +FACEBOOK_ACCOUNT_ERROR=Facebook account could not be verified +FACEBOOK_INVALID=Facebook login is invalid, please login again +APPLE_NETWORK_ERROR=Apple network error +MISS_PARAM_ERROR=Missing parameter +DISCORD_NETWORK_ERROR=Discord network error +GOOGLE_ID_GET_ERROR=Google ID get error +DEVICE_BLOCK_ERROR=This device has been banned +FREEZE_ERROR=Account has been frozen \ No newline at end of file diff --git a/sonic-pigeon/server/src/main/resources/messages_en.properties b/sonic-pigeon/server/src/main/resources/messages_en.properties new file mode 100644 index 0000000..24cec7c --- /dev/null +++ b/sonic-pigeon/server/src/main/resources/messages_en.properties @@ -0,0 +1,20 @@ +NO_LOGIN=User is not logged in +USER_NOT_EXIST=User does not exist +APP_CLIENT_NOT_EXIST=Login client does not exist +APP_CLIENT_NOT_ALLOW=Not authorized to log in to the client +SESSION_EXPIRED=Session is expired +SESSION_INVALID=Session is invalid +PASSWORD_INVALID=Invalid account or password +AUTH_FAIL=Authorization failed +USER_FROZEN=User is frozen +SYS_PARAMETERS_VALIDATE_EXCEPTION=Parameter validation error +SYS_VALIDATION_FAILED_ERROR=Validation failed, invalid request +ACCOUNT_NOT_REGISTER=Incorrect username or password +FACEBOOK_ACCOUNT_ERROR=Facebook account could not be verified +FACEBOOK_INVALID=Facebook login is invalid, please login again +APPLE_NETWORK_ERROR=Apple network error +MISS_PARAM_ERROR=Missing parameter +DISCORD_NETWORK_ERROR=Discord network error +GOOGLE_ID_GET_ERROR=Google ID get error +DEVICE_BLOCK_ERROR=This device has been banned +FREEZE_ERROR=Account has been frozen \ No newline at end of file diff --git a/sonic-shark/.gitignore b/sonic-shark/.gitignore new file mode 100644 index 0000000..dd701a1 --- /dev/null +++ b/sonic-shark/.gitignore @@ -0,0 +1,28 @@ +/**/rebel.xml + +#target/ +target/ + +# IDEA # +.idea/ +*.iml + +# Eclipse # +.settings/ +.classpath +.project + +# Log # +logs/ +*.log% + +#mac +.DS_Store + +#svn +.svn +#reviewboardrc +.reviewboardrc +**/rest-client.private.env.json + +.lingma/ diff --git a/sonic-shark/README.md b/sonic-shark/README.md new file mode 100644 index 0000000..8806b91 --- /dev/null +++ b/sonic-shark/README.md @@ -0,0 +1,2 @@ +# sonic-oss + diff --git a/sonic-shark/bootstrap-guide.md b/sonic-shark/bootstrap-guide.md new file mode 100644 index 0000000..ba12dc6 --- /dev/null +++ b/sonic-shark/bootstrap-guide.md @@ -0,0 +1,41 @@ +# 项目定制化手册 +## 定制化步骤 +* 确定项目依赖的组件比如redis,mysql,rabbitmq, 然后搜索`TODO`把不需要的依赖和多余的目录移除. +* 确定项目依赖组件后, 请在application-${env}中配置对应的资源地址. +* 运行单元测试, 保证单元测试全部通过. + +## 模块介绍 +### common +在该模块添加其他模块共用的lib,例如common-lib以及常用的guava,fastjson等
+主要是考虑到项目可能有多个部署的模块,通过将共用的lib定义在common模块中,可以简化其他模块的配置 + +### server +可部署的后端服务,包含SpringBoot的入口以及该服务相关的client,config,entity,dao, service,controller等 + +#### config +定义配置信息和错误code + +#### client +定义访问依赖的第三方服务的客户端接口. 访问依赖方服务,必须通过Client接口封装,禁止业务代码调用http相关逻辑. + +#### entity +定义领域对象. + +#### service +主要定义业务逻辑代码 + +#### controller +对外暴露的API定义 + +#### test +单元测试模块. 为了保证交付的质量和服务的演进,核心逻辑需要编写单元测试, + +##### 目录文件 +- java + - ClientStubs 第三方依赖客户端的Stub实现. + - BaseTest 单元测试基类. 建议每个单元测试从它基础 +- resources + - mysql 存放数据库的schema和测试数据. schema文件可以作为schema变化的版本记录, 同时也是H2数据库初始化脚本. + +### integration-test +集成测试,测试已部署服务的APIs diff --git a/sonic-shark/common/pom.xml b/sonic-shark/common/pom.xml new file mode 100644 index 0000000..7ed84e2 --- /dev/null +++ b/sonic-shark/common/pom.xml @@ -0,0 +1,54 @@ + + + + sonic-shark + com.sonic.shark + 1.0 + + 4.0.0 + + sonic-shark-common + jar + 1.0 + + + + com.sonic + common-lib + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + mysql + mysql-connector-java + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.google.guava + guava + + + com.alibaba + fastjson + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-pool2 + + + diff --git a/sonic-shark/common/src/main/java/com/sonic/shark/common/GlobalConfig.java b/sonic-shark/common/src/main/java/com/sonic/shark/common/GlobalConfig.java new file mode 100644 index 0000000..6d8241d --- /dev/null +++ b/sonic-shark/common/src/main/java/com/sonic/shark/common/GlobalConfig.java @@ -0,0 +1,106 @@ +package com.sonic.shark.common; + +import com.alibaba.fastjson.JSONObject; +import com.sonic.common.AppRuntime; +import com.sonic.common.log.RequestLogAspect; +import com.sonic.common.rpc.HttpClient; +import com.sonic.common.rpc.RpcClient; +import com.sonic.common.rpc.RpcClientImpl; +import com.sonic.common.stat.FrequencyAlertInterceptor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.task.TaskExecutorBuilder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.stream.Collectors; + +import static org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME; + +/** + * @author code + */ +@Slf4j +public class GlobalConfig { + @Value("${spring.application.name}") + private String appName; + @Value("${spring.application.id}") + private String appId; + + @Bean + ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() { + return new ScheduledThreadPoolExecutor(1); + } + + /** + * XXX Spring默认配置当有Executor的bean后不再装载TaskExecutor, 这里因为手动注册了 + * {@link #scheduledThreadPoolExecutor}会导致spring不再自动注册TaskExecutor, 因此需要去掉ConditionalOnMissingBean的限制. + * 参考{@link TaskExecutionAutoConfiguration#applicationTaskExecutor} + */ + @Bean(name = {APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME}) + public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { + return builder.build(); + } + + @Bean + public RequestLogAspect requestLogAspect() { + return new RequestLogAspect(); + } + + @Bean + AppRuntime appRuntime(ApplicationContext applicationContext) { + return AppRuntime.builder().appId(appId).appName(appName).applicationContext(applicationContext).build(); + } + + /** + * 用于配置接口访问频率超限告警, 接口频率配置参考application.yml中的web.frequency-alert.rules + * TODO 根据需要配置为输出到日志或者alertClient + * + * @return + */ + @Bean + public FrequencyAlertInterceptor frequencyAlertInterceptor(RuleConfig ruleConfig) { + return new FrequencyAlertInterceptor(ruleConfig.getAlertRuleMap(), + (request, frequencyAlert) -> log.warn("frequency alert, api frequency alert, url = {} alert = {}", + request.getRequestURI(), JSONObject.toJSONString(frequencyAlert))); + } + + @Bean + @Primary + public RpcClient rpcClient() { + return new RpcClientImpl(HttpClient.Config.EMPTY); + } + + @Data + @Component + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "web.frequency-alert") + public static class RuleConfig { + private List rules; + + public Map getAlertRuleMap() { + if (rules == null) { + return Collections.emptyMap(); + } + return rules.stream() + .map(e -> { + String[] split = e.split(":"); + return new AbstractMap.SimpleEntry<>(split[0], split[1]); + }).collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); + } + } +} diff --git a/sonic-shark/common/src/main/java/com/sonic/shark/common/MybatisPlusConfig.java b/sonic-shark/common/src/main/java/com/sonic/shark/common/MybatisPlusConfig.java new file mode 100644 index 0000000..f0cd935 --- /dev/null +++ b/sonic-shark/common/src/main/java/com/sonic/shark/common/MybatisPlusConfig.java @@ -0,0 +1,19 @@ +package com.sonic.shark.common; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; + +@EnableTransactionManagement +@MapperScan("com.sonic.**.dao") +public class MybatisPlusConfig { + + @Bean + public PaginationInterceptor paginationInterceptor() { + PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); + paginationInterceptor.setDialectType("mysql"); + return paginationInterceptor; + } +} diff --git a/sonic-shark/lib/pom.xml b/sonic-shark/lib/pom.xml new file mode 100644 index 0000000..d824bc3 --- /dev/null +++ b/sonic-shark/lib/pom.xml @@ -0,0 +1,55 @@ + + + + sonic-shark + com.sonic.shark + 1.0 + + 4.0.0 + + com.sonic.shark + sonic-shark-lib + jar + 1.0-SNAPSHOT + + + + + com.sonic + common-lib + + + + log4j-api + org.apache.logging.log4j + + + com.alibaba + fastjson + + + + + + com.alibaba + fastjson + 1.2.83 + + + + + + + + releases + http://121.196.56.236:8081/repository/maven-releases/ + + + snapshots + http://121.196.56.236:8081/repository/maven-snapshots/ + + + + \ No newline at end of file diff --git a/sonic-shark/lib/src/main/java/com/sonic/shark/lib/client/S3BlurryImgClient.java b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/client/S3BlurryImgClient.java new file mode 100644 index 0000000..0a0ec54 --- /dev/null +++ b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/client/S3BlurryImgClient.java @@ -0,0 +1,148 @@ +package com.sonic.shark.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.Lists; +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.shark.lib.input.BlurryRecordInput; +import com.sonic.shark.lib.input.CopyAndBlurryImgInput; +import com.sonic.shark.lib.output.BlurryRecordOutput; +import com.sonic.shark.lib.output.CopyAndBlurryImgOutput; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author code + */ +@Slf4j +@Service +public class S3BlurryImgClient { + + private static final String COPY_AND_BLURRY_IMG_URL = "/api/img/copy-and-blurry"; + + private static final String BLURRY_RECORD_LIST_URL = "/api/img/blurry-record-list"; + + private RpcClient rpcClient; + private String host; + + public S3BlurryImgClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://dev-shark.svc.cloud"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-shark-svc:8080"; + break; + case product: + default: + this.host = "http://prod-shark-svc:8080"; + } + } + + + /** + * 拷贝并生成模糊图片访问地址 + * @param input + * @return 返回原图地址 + */ + public String copyAndBlurryImg(CopyAndBlurryImgInput input) { + List inputList = Lists.newArrayList(); + inputList.add(input); + List list = rpcClient.postBodySign(host + COPY_AND_BLURRY_IMG_URL, input, new TypeReference>>(){}); + if(CollectionUtils.isEmpty(list)) { + return null; + } + return list.get(0).getImgUrl(); + } + + /** + * 批量拷贝相册图片并生成模糊图片访问地址 key是业务类型ID + * @param inputList + * @return + */ + public Map copyAndBlurryAlbumImg(List inputList) { + List list = rpcClient.postBodySign(host + COPY_AND_BLURRY_IMG_URL, inputList, new TypeReference>>(){}); + Map resultMap = list.stream().collect(Collectors.toMap(e -> e.getBizId(), e -> e.getImgUrl())); + return resultMap; + } + + /** + * 批量拷贝相册图片并生成模糊图片访问地址 key是图片相对路径ID + * @param inputList + * @return + */ + public Map copyAndBlurryTopicImg(List inputList) { + List list = rpcClient.postBodySign(host + COPY_AND_BLURRY_IMG_URL, inputList, new TypeReference>>(){}); + Map resultMap = list.stream().collect(Collectors.toMap(e -> e.getSourceImgPath(), e -> e.getImgUrl())); + return resultMap; + } + + /** + * 批量拷贝相册图片并生成模糊图片访问地址 + * @param input + * @return + */ + public CopyAndBlurryImgOutput copyAndBlurryTopicImg(CopyAndBlurryImgInput input) { + List inputList = Lists.newArrayList(input); + List list = rpcClient.postBodySign(host + COPY_AND_BLURRY_IMG_URL, inputList, new TypeReference>>(){}); + return CollectionUtils.isEmpty(list) ? null : list.get(0); + } + + /** + * 批量查询模糊图片数据 key是相对路径 + * @param bizType + * @param bizIdList + * @return + */ + public Map blurryRecordImgPathMap(String bizType, List bizIdList) { + BlurryRecordInput input = new BlurryRecordInput(); + input.setBizType(bizType); + input.setBizIdList(bizIdList); + List list = rpcClient.postBodySign(host + BLURRY_RECORD_LIST_URL, input, new TypeReference>>(){}); + return list.stream().collect(Collectors.toMap(e -> e.getImgPath(), Function.identity())); + } + + /** + * 批量查询模糊图片数据 key是全路径 + * @param bizType + * @param bizIdList + * @return + */ + public Map blurryRecordImgUrlMap(String bizType, List bizIdList) { + BlurryRecordInput input = new BlurryRecordInput(); + input.setBizType(bizType); + input.setBizIdList(bizIdList); + input.setImgUrlBl(true); + List list = rpcClient.postBodySign(host + BLURRY_RECORD_LIST_URL, input, new TypeReference>>(){}); + return list.stream().collect(Collectors.toMap(e -> e.getImgUrl(), Function.identity())); + } + + /** + * 批量查询模糊图片数据 key是业务类型ID + * @param bizType + * @param bizIdList + * @return + */ + public Map blurryRecordBizIdMap(String bizType, List bizIdList) { + BlurryRecordInput input = new BlurryRecordInput(); + input.setBizType(bizType); + input.setBizIdList(bizIdList); + input.setImgUrlBl(false); + List list = rpcClient.postBodySign(host + BLURRY_RECORD_LIST_URL, input, new TypeReference>>(){}); + return list.stream().collect(Collectors.toMap(e -> e.getBizId(), Function.identity())); + } + + +} diff --git a/sonic-shark/lib/src/main/java/com/sonic/shark/lib/client/S3CheckClient.java b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/client/S3CheckClient.java new file mode 100644 index 0000000..041a3df --- /dev/null +++ b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/client/S3CheckClient.java @@ -0,0 +1,95 @@ +package com.sonic.shark.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.Lists; +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.shark.lib.input.ImageCheckInput; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.List; + +/** + * @author code + */ +@Slf4j +@Service +public class S3CheckClient { + + private static final String URI_S3_CHECK_CHECK_IMAGE = "/api/s3-check/check-image"; + + private RpcClient rpcClient; + private String host; + + public S3CheckClient(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://dev-shark.svc.cloud"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-shark-svc:8080"; + break; + case product: + default: + this.host = "http://prod-shark-svc:8080"; + } + } + + /** + * 图片校验 + * @param url + */ + public void checkImage(String url) { + checkImage(null, url); + } + + /** + * 图片校验 + * @param urlList + * @return + */ + public void checkImage(List urlList) { + checkImage(null, urlList); + } + + /** + * 图片校验 + * @param userId + * @param url + */ + public void checkImage(Long userId, String url) { + if(StringUtils.isEmpty(url)) { + return; + } + ImageCheckInput input = new ImageCheckInput(); + input.setUrls(Lists.newArrayList(url)); + input.setUserId(userId); + rpcClient.post(host + URI_S3_CHECK_CHECK_IMAGE, input, new TypeReference>(){}); + } + + /** + * 图片校验 + * @param userId + * @param urlList + * @return + */ + public void checkImage(Long userId, List urlList) { + if(CollectionUtils.isEmpty(urlList)) { + return; + } + ImageCheckInput input = new ImageCheckInput(); + input.setUserId(userId); + input.setUrls(urlList); + rpcClient.post(host + URI_S3_CHECK_CHECK_IMAGE, input, new TypeReference>(){}); + } + +} diff --git a/sonic-shark/lib/src/main/java/com/sonic/shark/lib/client/S3Client.java b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/client/S3Client.java new file mode 100644 index 0000000..7d955e5 --- /dev/null +++ b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/client/S3Client.java @@ -0,0 +1,68 @@ +package com.sonic.shark.lib.client; + +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.Lists; +import com.sonic.common.AppRuntime; +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.rpc.RpcClient; +import com.sonic.shark.lib.input.ImageCheckInput; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author code + */ +@Slf4j +@Service +public class S3Client { + + private static final String UPLOAD_IMG_TO_AWS_URL = "/api/oss/upload/aws/s3"; + private static final String NSFW_RECOGNITION_URL = "/api/nsfw/recognition"; + + private RpcClient rpcClient; + private String host; + + public S3Client(RpcClient rpcClient, AppRuntime appRuntime) { + this.rpcClient = rpcClient; + switch (appRuntime.getEnv()) { + case dev: + this.host = "http://dev-shark.svc.cloud"; + break; + case local: + this.host = "http://localhost:8080"; + break; + case test: + this.host = "http://test-shark-svc:8080"; + break; + case product: + default: + this.host = "http://prod-shark-svc:8080"; + } + } + + public String uploadAwsS3(byte[] bytes, String bizTypeEnum, String suffix) { + Map param = new HashMap<>(3); + param.put("bytes", bytes); + param.put("bizTypeEnum", bizTypeEnum); + param.put("suffix", suffix); + String resultUrl = rpcClient.postBodySign(host + UPLOAD_IMG_TO_AWS_URL, param, new TypeReference>() { + }); + return resultUrl; + } + + public Boolean nsfwRecognition(String fileFullPath, String bizTypeEnum) { + Map param = new HashMap<>(3); + param.put("fileFullPath", fileFullPath); + param.put("bizTypeEnum", bizTypeEnum); + return rpcClient.postBodySign(host + NSFW_RECOGNITION_URL, param, new TypeReference>() { + }); + } + +} diff --git a/sonic-shark/lib/src/main/java/com/sonic/shark/lib/enums/BlurryImgBizTypeEnum.java b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/enums/BlurryImgBizTypeEnum.java new file mode 100644 index 0000000..9be66a8 --- /dev/null +++ b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/enums/BlurryImgBizTypeEnum.java @@ -0,0 +1,6 @@ +package com.sonic.shark.lib.enums; + +public enum BlurryImgBizTypeEnum { + ALBUM, + ; +} diff --git a/sonic-shark/lib/src/main/java/com/sonic/shark/lib/input/BlurryRecordInput.java b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/input/BlurryRecordInput.java new file mode 100644 index 0000000..f0f11f9 --- /dev/null +++ b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/input/BlurryRecordInput.java @@ -0,0 +1,52 @@ +package com.sonic.shark.lib.input; + +import java.util.List; + +/** + * @Author code + * @Description 拷贝并模糊图片 + */ +public class BlurryRecordInput { + + /** + * 业务类型 + */ + private String bizType; + + /** + * 业务ID + */ + private List bizIdList; + + /** + * 是否返回完整的URL地址 + */ + private Boolean imgUrlBl; + + public BlurryRecordInput() { + } + + public String getBizType() { + return bizType; + } + + public void setBizType(String bizType) { + this.bizType = bizType; + } + + public List getBizIdList() { + return bizIdList; + } + + public void setBizIdList(List bizIdList) { + this.bizIdList = bizIdList; + } + + public Boolean getImgUrlBl() { + return imgUrlBl; + } + + public void setImgUrlBl(Boolean imgUrlBl) { + this.imgUrlBl = imgUrlBl; + } +} diff --git a/sonic-shark/lib/src/main/java/com/sonic/shark/lib/input/CopyAndBlurryImgInput.java b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/input/CopyAndBlurryImgInput.java new file mode 100644 index 0000000..50b391a --- /dev/null +++ b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/input/CopyAndBlurryImgInput.java @@ -0,0 +1,78 @@ +package com.sonic.shark.lib.input; + +import com.sonic.shark.lib.enums.BlurryImgBizTypeEnum; + +/** + * @Author code + * @Description 拷贝并模糊图片 + */ +public class CopyAndBlurryImgInput { + + /** + * 原图片的相对路径 + */ + private String sourceFilePath; + + /** + * 图片的全路径 + */ + private String sourceFileUrl; + + /** + * 用户ID,用户在拷贝图片时存放到用户的目录中 + */ + private Long userId; + + /** + * 业务类型(ALBUM) + */ + private BlurryImgBizTypeEnum bizType; + + /** + * 业务ID + */ + private Long bizId; + + public CopyAndBlurryImgInput() { + } + + public String getSourceFilePath() { + return sourceFilePath; + } + + public void setSourceFilePath(String sourceFilePath) { + this.sourceFilePath = sourceFilePath; + } + + public String getSourceFileUrl() { + return sourceFileUrl; + } + + public void setSourceFileUrl(String sourceFileUrl) { + this.sourceFileUrl = sourceFileUrl; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public BlurryImgBizTypeEnum getBizType() { + return bizType; + } + + public void setBizType(BlurryImgBizTypeEnum bizType) { + this.bizType = bizType; + } + + public Long getBizId() { + return bizId; + } + + public void setBizId(Long bizId) { + this.bizId = bizId; + } +} diff --git a/sonic-shark/lib/src/main/java/com/sonic/shark/lib/input/ImageCheckInput.java b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/input/ImageCheckInput.java new file mode 100644 index 0000000..257be2a --- /dev/null +++ b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/input/ImageCheckInput.java @@ -0,0 +1,22 @@ +package com.sonic.shark.lib.input; + +import io.swagger.annotations.ApiParam; +import lombok.Data; + +import java.util.List; + +/** + * @Author zzhan + * @Date 2020/12/1 12:12 + * @Version 1.0 + */ +@Data +public class ImageCheckInput { + + @ApiParam(value = "用户ID") + Long userId; + + @ApiParam(value = "图片url地址列表") + List urls; + +} diff --git a/sonic-shark/lib/src/main/java/com/sonic/shark/lib/output/BlurryRecordOutput.java b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/output/BlurryRecordOutput.java new file mode 100644 index 0000000..f2200f2 --- /dev/null +++ b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/output/BlurryRecordOutput.java @@ -0,0 +1,90 @@ +package com.sonic.shark.lib.output; + + +/** + * @Author code + * @Description 查询模糊图片数据出参对象 + */ +public class BlurryRecordOutput { + + /** + * 业务主键ID + */ + private Long bizId; + + /** + * 图片的相对路径 + */ + private String imgPath; + + /** + * 图片的全路径 + */ + private String imgUrl; + + /** + * 模糊图片地址全路径(尺寸:468*600) + */ + private String img1; + + /** + * 模糊图片地址全路径(尺寸:800*800) + */ + private String img2; + + /** + * 模糊图片地址全路径(备用) + */ + private String img3; + + public BlurryRecordOutput() { + } + + public Long getBizId() { + return bizId; + } + + public void setBizId(Long bizId) { + this.bizId = bizId; + } + + public String getImgPath() { + return imgPath; + } + + public void setImgPath(String imgPath) { + this.imgPath = imgPath; + } + + public String getImgUrl() { + return imgUrl; + } + + public void setImgUrl(String imgUrl) { + this.imgUrl = imgUrl; + } + + public String getImg1() { + return img1; + } + + public void setImg1(String img1) { + this.img1 = img1; + } + + public String getImg2() { + return img2; + } + + public void setImg2(String img2) { + this.img2 = img2; + } + + public String getImg3() { + return img3; + } + + public void setImg3(String img3) { + this.img3 = img3; + } +} diff --git a/sonic-shark/lib/src/main/java/com/sonic/shark/lib/output/CopyAndBlurryImgOutput.java b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/output/CopyAndBlurryImgOutput.java new file mode 100644 index 0000000..ee5f280 --- /dev/null +++ b/sonic-shark/lib/src/main/java/com/sonic/shark/lib/output/CopyAndBlurryImgOutput.java @@ -0,0 +1,103 @@ +package com.sonic.shark.lib.output; + + +/** + * @Author code + * @Description 拷贝并模糊图片 + */ +public class CopyAndBlurryImgOutput { + + /** + * 业务ID + */ + private Long bizId; + + /** + * 图片的全路径 + */ + private String imgUrl; + + /** + * 图片拷贝前的原图的路径 + */ + private String sourceImgPath; + + /** + * 图片的相对路径 + */ + private String imgPath; + + /** + * 图片1 + */ + private String img1; + + /** + * 图片2 + */ + private String img2; + + /** + * 图片3 + */ + private String img3; + + public CopyAndBlurryImgOutput() { + } + + public Long getBizId() { + return bizId; + } + + public void setBizId(Long bizId) { + this.bizId = bizId; + } + + public String getImgUrl() { + return imgUrl; + } + + public void setImgUrl(String imgUrl) { + this.imgUrl = imgUrl; + } + + public String getSourceImgPath() { + return sourceImgPath; + } + + public void setSourceImgPath(String sourceImgPath) { + this.sourceImgPath = sourceImgPath; + } + + public String getImgPath() { + return imgPath; + } + + public void setImgPath(String imgPath) { + this.imgPath = imgPath; + } + + public String getImg1() { + return img1; + } + + public void setImg1(String img1) { + this.img1 = img1; + } + + public String getImg2() { + return img2; + } + + public void setImg2(String img2) { + this.img2 = img2; + } + + public String getImg3() { + return img3; + } + + public void setImg3(String img3) { + this.img3 = img3; + } +} diff --git a/sonic-shark/pom.xml b/sonic-shark/pom.xml new file mode 100644 index 0000000..3619ffd --- /dev/null +++ b/sonic-shark/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + com.sonic + common-parent-pom + 1.0 + + + sonic-shark + + com.sonic.shark + pom + 1.0 + + + + 1.0.6 + 1.0 + + + + + + com.sonic + common-lib + ${common-lib.version} + + + com.sonic + dao-support-lib + ${dao-support-lib.version} + + + + + + + org.projectlombok + lombok + + + + + common + server + lib + + diff --git a/sonic-shark/server/pom.xml b/sonic-shark/server/pom.xml new file mode 100644 index 0000000..204dacc --- /dev/null +++ b/sonic-shark/server/pom.xml @@ -0,0 +1,244 @@ + + + + sonic-shark + com.sonic.shark + 1.0 + + 4.0.0 + + sonic-shark-server + jar + + + + com.sonic.shark + sonic-shark-common + 1.0 + + + + com.sonic.sdk + sonic-common-api + 1.0.1 + + + + com.sonic + dao-support-lib + 1.0 + + + + com.sonic.shark + sonic-shark-lib + 1.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + it.ozimov + embedded-redis + + + + + org.redisson + redisson-spring-boot-starter + 3.13.1 + + + + + org.springframework.amqp + spring-amqp + + + org.springframework.amqp + spring-rabbit + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + + + com.h2database + h2 + + + + software.amazon.awssdk + s3 + 2.10.54 + + + + com.amazonaws + aws-java-sdk-rekognition + 1.12.792 + + + + + com.amazonaws + aws-java-sdk-sts + 1.12.792 + + + aws-java-sdk-core + com.amazonaws + + + jmespath-java + com.amazonaws + + + + + + com.amazonaws + aws-java-sdk-s3 + 1.12.792 + + + aws-java-sdk-core + com.amazonaws + + + aws-java-sdk-kms + com.amazonaws + + + jmespath-java + com.amazonaws + + + + + + aws-java-sdk-core + + + jackson-databind + com.fasterxml.jackson.core + + + joda-time + joda-time + + + com.amazonaws + + + + aws-java-sdk-kms + + + aws-java-sdk-core + com.amazonaws + + + jmespath-java + com.amazonaws + + + com.amazonaws + 1.12.792 + + + jmespath-java + + + jackson-databind + com.fasterxml.jackson.core + + + com.amazonaws + 1.12.792 + + + + org.hashids + hashids + 1.0.3 + + + software.amazon.awssdk + regions + 2.10.54 + compile + + + jackson-databind + com.fasterxml.jackson.core + + + + + + + + + + + + joda-time + joda-time + 2.9.4 + compile + + + + + + + + + com.amazonaws + jmespath-java + 1.12.792 + + + + + com.amazonaws + aws-java-sdk-core + 1.12.792 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pl.project13.maven + git-commit-id-plugin + + + false + false + + + + + diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/MainApplication.java b/sonic-shark/server/src/main/java/com/sonic/shark/MainApplication.java new file mode 100644 index 0000000..4c7b34d --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/MainApplication.java @@ -0,0 +1,22 @@ +package com.sonic.shark; + +import com.sonic.sdk.api.annotation.EnableDecrypt; +import com.sonic.sdk.api.annotation.EnableGatWayAuthScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * @author code + */ +@EnableSwagger2 +@ComponentScan(value = {"com.sonic"}) +@SpringBootApplication +@EnableGatWayAuthScan(basePackages = "com.sonic.shark.controller") +@EnableDecrypt +public class MainApplication { + public static void main(String[] args) { + SpringApplication.run(MainApplication.class, args); + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/config/AwsS3Config.java b/sonic-shark/server/src/main/java/com/sonic/shark/config/AwsS3Config.java new file mode 100644 index 0000000..a8e1386 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/config/AwsS3Config.java @@ -0,0 +1,30 @@ +package com.sonic.shark.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +/** + * code + **/ +@Configuration +public class AwsS3Config { + + @Value("${aws.s3.fileOpt.accessKeyId}") + private String awsId; + @Value("${aws.s3.fileOpt.secretAccessKey}") + private String awsKey; + + @Bean + public S3Client s3client() { + AwsBasicCredentials awsCreds = AwsBasicCredentials.create(awsId, awsKey); + S3Client s3Client = S3Client.builder() + .region(Region.US_WEST_2) + .credentialsProvider(StaticCredentialsProvider.create(awsCreds)).build(); + return s3Client; + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/config/Config.java b/sonic-shark/server/src/main/java/com/sonic/shark/config/Config.java new file mode 100644 index 0000000..2b7ccf5 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/config/Config.java @@ -0,0 +1,48 @@ +package com.sonic.shark.config; + +import com.sonic.common.AppRuntime; +import com.sonic.common.config.DefaultWebMvcConfig; +import com.sonic.common.exception.AbstractExceptionHandler; +import com.sonic.shark.common.GlobalConfig; +import com.sonic.shark.common.MybatisPlusConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; + +import java.util.function.Consumer; + +/** + * Server服务唯一的配置入口 + * TODO: Import的各种Config,按需使用(例如,如果不需要mysql,kafka消息,redis等,可以去掉对应的Config) + * + * @author code + */ +@Slf4j +@Configuration +@Import({GlobalConfig.class, DefaultWebMvcConfig.class, MybatisPlusConfig.class, RedisConfig.class, + EventConfig.class, SwaggerConfig.class}) +public class Config { + + /** + * XXX: ExceptionHandlerConfig 应该在 DefaultWebMvcConfig 前被初始. 否则 DefaultWebMvcConfig 的 exceptionHandlerHook 会生效 + */ + static class ExceptionHandlerConfig { + @Bean + @Profile({"!unittest && !local"}) + AbstractExceptionHandler.ExceptionHandlerHook exceptionHandlerHook(AppRuntime appRuntime) { + return new AbstractExceptionHandler.ExceptionHandlerHook() { + @Override + public String getAppId() { + return appRuntime.getAppId(); + } + + @Override + public Consumer getExceptionConsumer() { + return context -> log.error("未捕获内部异常, logText: {}", context.dumpRequest(), context.getException()); + } + }; + } + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/config/EventConfig.java b/sonic-shark/server/src/main/java/com/sonic/shark/config/EventConfig.java new file mode 100644 index 0000000..7ef79f2 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/config/EventConfig.java @@ -0,0 +1,125 @@ +package com.sonic.shark.config; + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.ImmutableMap; +import com.rabbitmq.client.Channel; +import com.sonic.common.AppRuntime; +import com.sonic.common.event.*; +import com.sonic.common.utils.LogUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.annotation.RabbitListeners; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Payload; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * @author code + */ +@Slf4j +public class EventConfig { + + /** TODO: 定义 Event.BuildInScene */ + public final static String DEFAULT_SCENE = Event.BuildInScene.SONIC.getCode(); + /** TODO: DEFAULT_SCENE + "_" + appName */ + public final static String DEFAULT_MODULE = DEFAULT_SCENE + "_" + "sonic"; + + @Value("${mq.exchange}") + private String mqExchange; + @Value("${mq.default.queue}") + private String defaultQueue; + @Value("${mq.default.routing-key}") + private String defaultRoutingKey; + + @Configuration + @Profile("!unittest") + static class Listener { + @Autowired + EventConsumer eventConsumer; + + @RabbitListeners({ + @RabbitListener(queues = {"${mq.default.queue}"}, concurrency = "2"), + }) + public void process(@Payload Message message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) { + try { + LogUtils.setTraceId(); + byte[] msgBody = message.getBody(); + String contentEncoding = message.getMessageProperties().getContentEncoding(); + String bodyStr = new String(msgBody, Charset.forName(ObjectUtils.defaultIfNull(contentEncoding, "UTF-8"))); + MessageProperties pro = message.getMessageProperties(); + if (log.isDebugEnabled()) { + log.info("receive msg routingKey:{}->consumerQueue:{}->redelivered:{}->body:{}", + pro.getReceivedRoutingKey(), pro.getConsumerQueue(), pro.getRedelivered(), bodyStr); + } + eventConsumer.onEvent(bodyStr, ImmutableMap.of("record", JSON.toJSONString(message))); + + //消息确认,multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息 + channel.basicAck(deliveryTag, false); + } catch (Exception e) { + log.error("===> mq handler error, message: {}", JSON.toJSONString(message), e); + //消息拒绝,//requeue=true,表示将消息重新放入到队列中,false:表示直接从队列中删除,此时和basicAck(long deliveryTag, false)的效果一样 + try { + channel.basicReject(deliveryTag,true); + } catch (IOException ioException) { + log.error("channel.basicReject error. message: {}, deliveryTag: {}", JSON.toJSONString(message), deliveryTag, ioException); + } + } finally { + LogUtils.removeTraceId(); + } + } + + } + + @Bean + EventProducer eventProducer(AppRuntime appRuntime, RabbitTemplate rabbitTemplate, TaskExecutor taskExecutor) { + BiConsumer callback = (event, meta) -> {}; + return new RabbitmqEventProducer(rabbitTemplate, DEFAULT_MODULE, DEFAULT_SCENE, appRuntime.getAppId(), + RabbitmqEventProducer.RabbitmqMessageMeta.build(mqExchange, defaultRoutingKey), taskExecutor, callback); + } + + @Bean + EventConsumer eventConsumer(AppRuntime appRuntime, EventHandlerRepository eventHandlerRepository) { + Consumer callback = (eventWrapper) -> {}; + return new DefaultEventConsumer(appRuntime.getAppName(), eventHandlerRepository, callback); + } + + @Bean + EventHandlerRepository eventHandlerRepository() { + return new EventHandlerRepository((ex, logText) -> log.error("MQ,handle error {}", logText, ex)); + } + + @Bean + public DirectExchange messageServerExchange(){ + return new DirectExchange(mqExchange); + } + + @Bean + public Binding bindingDefaultQueueExchange(DirectExchange exchange, Queue defaultQueue) { + return bindingExchange(exchange, defaultQueue, defaultRoutingKey); + } + + @Bean + public Queue defaultQueue() { + return new Queue(defaultQueue,true); + } + + public Binding bindingExchange(DirectExchange exchange, Queue queueMessage, String routingKey) { + return BindingBuilder.bind(queueMessage).to(exchange).with(routingKey); + } + +} + diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/config/RedisConfig.java b/sonic-shark/server/src/main/java/com/sonic/shark/config/RedisConfig.java new file mode 100644 index 0000000..6c8ae7f --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/config/RedisConfig.java @@ -0,0 +1,39 @@ +package com.sonic.shark.config; + +import com.sonic.common.utils.RedisLock; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * 有关Redis的配置 + * + * TODO: 如果不需要Redis, 可以删除该文件并且在{@link Config}类@Import的注解中移除对RedisConfig的引用 + */ +@Slf4j +public class RedisConfig { + + /** + * redisWrapper用于分布式锁RedisLock + * + * @param redisTemplate + * @return + */ + @Bean + RedisLock.RedisWrapper redisWrapper(RedisTemplate redisTemplate) { + return new RedisLock.RedisWrapper() { + @Override + public void delete(String key) { + redisTemplate.delete(key); + } + + @Override + public boolean lock(String key, String value, long expireMills) { + return redisTemplate.opsForValue().setIfAbsent(key, value, expireMills, TimeUnit.MILLISECONDS); + } + }; + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/config/RedissonConfig.java b/sonic-shark/server/src/main/java/com/sonic/shark/config/RedissonConfig.java new file mode 100644 index 0000000..f190556 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/config/RedissonConfig.java @@ -0,0 +1,52 @@ +package com.sonic.shark.config; + +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + + /** + * 配置 RedissonClient,支持单机和集群模式 + */ + @Bean + public RedissonClient redissonClient(RedisProperties redisProperties) { + Config config = new Config(); + + // 设置 Netty 线程数,使用默认值 0(Redisson 默认使用 CPU 核心数的两倍) + config.setNettyThreads(0); + + // 判断是否配置了集群节点 + if (redisProperties.getCluster() != null && redisProperties.getCluster().getNodes() != null + && !redisProperties.getCluster().getNodes().isEmpty()) { + // 集群模式配置 + config.useClusterServers() + .setScanInterval(2000) // 集群状态扫描间隔,单位毫秒 + .addNodeAddress(redisProperties.getCluster().getNodes().stream() + .map(node -> "redis://" + node) + .toArray(String[]::new)) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null); + } else { + // 单机模式配置 + config.useSingleServer() + .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort()) + .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) + ? redisProperties.getPassword() : null) + .setDatabase(redisProperties.getDatabase()); + } + + // 如果启用了 SSL,设置 SSL 相关配置 + if (redisProperties.isSsl()) { + config.useSingleServer().setSslEnableEndpointIdentification(false); // 禁用主机名验证 + // 集群模式下,Redisson 会自动应用 SSL 配置 + } + + return Redisson.create(config); + } +} \ No newline at end of file diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/config/RestTemplateConfig.java b/sonic-shark/server/src/main/java/com/sonic/shark/config/RestTemplateConfig.java new file mode 100644 index 0000000..e65c472 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/config/RestTemplateConfig.java @@ -0,0 +1,24 @@ +package com.sonic.shark.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(ClientHttpRequestFactory factory){ + return new RestTemplate(factory); + } + + @Bean + public ClientHttpRequestFactory simpleClientHttpRequestFactory(){ + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setReadTimeout(5000);//单位为ms + factory.setConnectTimeout(5000);//单位为ms + return factory; + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/config/ResultCode.java b/sonic-shark/server/src/main/java/com/sonic/shark/config/ResultCode.java new file mode 100644 index 0000000..f35bafb --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/config/ResultCode.java @@ -0,0 +1,43 @@ +package com.sonic.shark.config; + +import com.sonic.common.rpc.ApiResultCode; +import lombok.Getter; + +/** + * @author code + */ +@Getter +public enum ResultCode implements ApiResultCode { + + /** + * TODO: 可以在此处扩展服务自身需要用到的错误码信息 + */ + BIZ_ERROR1("0000", "业务异常1"), + DEMO_CREATED_FAIL("0001", "新增Demo实体失败"); + + private final String errorCode; + private final String errorMsg; + + ResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/config/SsoConfig.java b/sonic-shark/server/src/main/java/com/sonic/shark/config/SsoConfig.java new file mode 100644 index 0000000..49f6f6e --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/config/SsoConfig.java @@ -0,0 +1,16 @@ +package com.sonic.shark.config; + +import com.sonic.common.auth.GateWaySessionInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SsoConfig { + + + @Bean + public GateWaySessionInterceptor gateWaySessionInterceptor() { + return new GateWaySessionInterceptor(); + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/config/SwaggerConfig.java b/sonic-shark/server/src/main/java/com/sonic/shark/config/SwaggerConfig.java new file mode 100644 index 0000000..96b7912 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/config/SwaggerConfig.java @@ -0,0 +1,110 @@ +package com.sonic.shark.config; + + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import springfox.documentation.RequestHandler; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; + +/** + * swagger配置 + * @author code + */ +@Slf4j +@EnableSwagger2 +public class SwaggerConfig { + + private static final String SPLIT = ","; + + @Value("${swagger.enabled:false}") + private Boolean swaggerEnabled; + + @Value("${swagger.base.package}") + private String basePackageValue; + + private final String DEFAULT_BASE_PACKAGE = "com.sonic.bs.controller"; + + + public static Predicate basePackage(final String basePackage) { + return input -> declaringClass(input).transform(handlerPackage(basePackage)).or(true); + } + + private static Optional> declaringClass(RequestHandler input) { + return Optional.fromNullable(input.declaringClass()); + } + + private static Function, Boolean> handlerPackage(final String basePackage) { + return input -> { + // 循环判断匹配 + for (String strPackage : basePackage.split(SPLIT)) { + boolean isMatch = input.getPackage().getName().startsWith(strPackage); + if (isMatch) { + return true; + } + } + return false; + }; + } + + @Bean + public Docket createRestApi() { + basePackageValue = null == basePackageValue || ("").equals(basePackageValue) ? DEFAULT_BASE_PACKAGE : basePackageValue; + log.info("===> swagger base package : ", basePackageValue); + + //在配置好的配置类中增加此段代码即可 + ParameterBuilder ticketPar = new ParameterBuilder(); + List pars = new ArrayList(); + ticketPar.name("_tk_") + //name表示名称,description表示描述 + .description("token") + .modelRef(new ModelRef("string")) + .parameterType("header") + .required(false) + .defaultValue("") + .build();//required表示是否必填,defaultvalue表示默认值 + + //添加完此处一定要把下边的带***的也加上否则不生效 + pars.add(ticketPar.build()); + + + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .enable(swaggerEnabled) + .select() + .apis(basePackage(basePackageValue)) + .paths(PathSelectors.any()) + .build() + //************把消息头添加 + .globalOperationParameters(pars); + } + + /** + * TODO: 更改文案配置 + * @return + */ + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("epal") + .description("epal API") + .version("1.0") + .contact(new Contact("epal", "", "admin.epal.gg")) + .build(); + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/controller/api/CopyAndBlurryImgApi.java b/sonic-shark/server/src/main/java/com/sonic/shark/controller/api/CopyAndBlurryImgApi.java new file mode 100644 index 0000000..b3dd4ab --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/controller/api/CopyAndBlurryImgApi.java @@ -0,0 +1,50 @@ +package com.sonic.shark.controller.api; + +import com.sonic.common.auth.IgnoreAuth; +import com.sonic.common.rpc.Result; +import com.sonic.sdk.api.annotation.InternalRpc; +import com.sonic.shark.lib.input.BlurryRecordInput; +import com.sonic.shark.lib.input.CopyAndBlurryImgInput; +import com.sonic.shark.lib.output.BlurryRecordOutput; +import com.sonic.shark.lib.output.CopyAndBlurryImgOutput; +import com.sonic.shark.service.BlurryImgRecordService; +import com.sonic.shark.service.CopyAndBlurryImgService; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 图片模糊处理 + * @Author code + */ +@InternalRpc(path = "/api/**") +@RestController +@Slf4j +public class CopyAndBlurryImgApi { + + @Autowired + private CopyAndBlurryImgService copyAndBlurryImgService; + @Autowired + private BlurryImgRecordService blurryImgRecordService; + + @IgnoreAuth + @ApiOperation(value = "拷贝并生成模糊图片访问地址", tags = {"API-图片处理"}) + @PostMapping(value = "/api/img/copy-and-blurry") + public Result> copyAndBlurryImg(@Validated @RequestBody List inputList) throws Exception { + return Result.success(copyAndBlurryImgService.copyAndBlurryImg(inputList)); + } + + @IgnoreAuth + @ApiOperation(value = "查询模糊图片访问地址", tags = {"API-图片处理"}) + @PostMapping(value = "/api/img/blurry-record-list") + public Result> copyAndBlurryImg(@Validated @RequestBody BlurryRecordInput input) { + return Result.success(blurryImgRecordService.queryBlurryImgList(input.getBizType(), input.getBizIdList(), input.getImgUrlBl())); + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/controller/api/S3Api.java b/sonic-shark/server/src/main/java/com/sonic/shark/controller/api/S3Api.java new file mode 100644 index 0000000..5354832 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/controller/api/S3Api.java @@ -0,0 +1,61 @@ +package com.sonic.shark.controller.api; + +import com.sonic.common.auth.IgnoreAuth; +import com.sonic.common.rpc.Result; +import com.sonic.shark.domain.bo.NsfwRecognitionResult; +import com.sonic.shark.domain.input.NsfwRecognitionInput; +import com.sonic.shark.domain.input.UploadAwsS3Input; +import com.sonic.shark.enums.S3BucketNameEnum; +import com.sonic.shark.service.ImageCheckService; +import com.sonic.shark.service.NsfwConfigService; +import com.sonic.shark.service.OssService; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + + +/** + * OSS文件上传内部API接口 + * + * @Author zzhan + * @Date 2020/9/23 21:41 + * @Version 1.0 + */ +@RestController +@Slf4j +public class S3Api { + + @Autowired + private OssService ossService; + @Autowired + private NsfwConfigService nsfwConfigService; + @Autowired + private ImageCheckService imageCheckService; + + + @IgnoreAuth + @ApiOperation(value = "通过文件地址上传到亚马逊s3", tags = {"API-文件持久化"}) + @PostMapping(value = "/api/oss/upload/aws/s3") + public Result uploadToAwsS3(@Validated @RequestBody UploadAwsS3Input uploadAwsS3Input) { + String s3Url = ossService.uploadFileBytes(uploadAwsS3Input.getBytes(), uploadAwsS3Input.getBizTypeEnum(), uploadAwsS3Input.getSuffix()); + return Result.success(s3Url); + } + + @IgnoreAuth + @ApiOperation(value = "图片鉴黄", tags = {"API-文件持久化"}) + @PostMapping(value = "/api/nsfw/recognition") + public Result nsfwRecognition(@Validated @RequestBody NsfwRecognitionInput input) { + S3BucketNameEnum s3BucketNameEnum = input.getBizTypeEnum().getS3BucketNameEnum(); + //图片鉴黄操作 + NsfwRecognitionResult nsfwRecognitionResult = nsfwConfigService.nsfwRecognition(s3BucketNameEnum, input.getFileFullPath()); + //鉴黄没通过 + if (nsfwRecognitionResult.getBl() != null && !nsfwRecognitionResult.getBl()) { + imageCheckService.addImageCheckResult(s3BucketNameEnum.getRootUrl() + input.getFileFullPath()); + } + return Result.success(nsfwRecognitionResult.getBl()); + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/controller/api/S3CheckApi.java b/sonic-shark/server/src/main/java/com/sonic/shark/controller/api/S3CheckApi.java new file mode 100644 index 0000000..743bb4a --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/controller/api/S3CheckApi.java @@ -0,0 +1,35 @@ +package com.sonic.shark.controller.api; + +import com.sonic.common.auth.IgnoreAuth; +import com.sonic.common.rpc.Result; +import com.sonic.shark.lib.input.ImageCheckInput; +import com.sonic.shark.service.ImageCheckService; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * S3文件检查接口 + * + * @Author code + */ +@RestController +@Slf4j +public class S3CheckApi { + + @Autowired + private ImageCheckService imageCheckService; + + @IgnoreAuth + @ApiOperation(value = "校验图片是否鉴黄通过,防止篡改绕过", tags = {"API-文件校验"}) + @PostMapping(value = "/api/s3-check/check-image") + public Result checkImage(@Validated @RequestBody ImageCheckInput input) { + imageCheckService.checkImageIsPassed(input.getUrls()); + return Result.success(); + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/controller/probe/ProbeController.java b/sonic-shark/server/src/main/java/com/sonic/shark/controller/probe/ProbeController.java new file mode 100644 index 0000000..218a29f --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/controller/probe/ProbeController.java @@ -0,0 +1,23 @@ +package com.sonic.shark.controller.probe; + +import com.sonic.common.auth.IgnoreAuth; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 探测接口 + */ +@RestController +@Slf4j +public class ProbeController { + + @IgnoreAuth + @ApiOperation(value = "检测服务存活状态", tags = {"探针"}) + @PostMapping("/probe/check") + public void probeCheck() { + + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/controller/web/S3Web.java b/sonic-shark/server/src/main/java/com/sonic/shark/controller/web/S3Web.java new file mode 100644 index 0000000..4d4f055 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/controller/web/S3Web.java @@ -0,0 +1,135 @@ +package com.sonic.shark.controller.web; + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.rpc.Result; +import com.sonic.common.utils.IpAddressUtils; +import com.sonic.sdk.api.annotation.IgnoreAuth; +import com.sonic.shark.domain.bo.NsfwRecognitionResult; +import com.sonic.shark.domain.bo.S3StsToken; +import com.sonic.shark.domain.input.GetNsfwResultInput; +import com.sonic.shark.domain.input.StsTokenInput; +import com.sonic.shark.enums.BizResultCode; +import com.sonic.shark.enums.S3BizTypeMappingEnum; +import com.sonic.shark.enums.S3BucketNameEnum; +import com.sonic.shark.enums.ToastResultCode; +import com.sonic.shark.limit.RequestLimit; +import com.sonic.shark.service.*; +import com.sonic.shark.utils.AwsS3Utils; +import com.sonic.shark.utils.Constants; +import com.sonic.shark.utils.LimitUtils; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; + +import javax.servlet.http.HttpServletRequest; + + +/** + * AWS S3 文件处理 + */ +@RestController +@Slf4j +public class S3Web { + + @Autowired + private S3StsService s3StsService; + + @Autowired + private FileUploadLogService fileUploadLogService; + + @Autowired + private AwsS3Utils awsS3Utils; + @Autowired + private NsfwConfigService nsfwConfigService; + @Autowired + private ImageCheckService imageCheckService; + @Autowired + private NsfwLogService nsfwLogService; + @Autowired + private LimitUtils limitUtils; + + /** + * 24小时内请求的数量达到了100次,直接限流 + */ + @IgnoreAuth + @RequestLimit(count = 500, time = 86400000) + @ApiOperation(value = "获取s3的sts的授权token", tags = {"WEB-S3文件"}) + @PostMapping(value = "/web/file/sts-tk") + public Result fileStsToken(@RequestBody StsTokenInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + //对用户做限流、鉴权的一系列操作 + boolean bl = limitUtils.limitCheckByBizAndUserId(session.getUserId(), "stsToken", 200); + ToastResultCode.SYS_OPERATOR_24_HOUR_ERROR.check(bl); + //校验枚举类型,只能是固定的枚举类型才能无需登录拉取token + ToastResultCode.STS_BIZ_TYPE_ERROR.check(session.getUserId() == null && input.getBizTypeEnum().getLoginCheck()); + //如果用户ID为空则存放到0的空间目录下 + session.setUserId(session.getUserId() == null ? 0L : session.getUserId()); + S3BucketNameEnum s3BucketNameEnum = input.getBizTypeEnum().getS3BucketNameEnum(); + S3StsToken s3StsToken = s3StsService.getStsToken(input.getBizTypeEnum(), session.getUserId(), input.getSuffix()); + s3StsToken.setBucket(s3BucketNameEnum.getBucketName()); + s3StsToken.setEndPoint(s3BucketNameEnum.getEndPoint()); + s3StsToken.setUrlPath(s3BucketNameEnum.getRootUrl() + s3StsToken.getPath()); + s3StsToken.setRegion(s3BucketNameEnum.getRegion().id()); + //保存数据到日志表中 + fileUploadLogService.save(session.getUserId(), s3StsToken, IpAddressUtils.getIpAddress(request), session.getEndpoint()); + return Result.success(s3StsToken); + } + + + @RequestLimit(count = 500, time = 86400000) + @ApiOperation(value = "获取鉴黄结果", tags = {"WEB-S3文件"}) + @PostMapping(value = "/web/file/check") + public Result fileNsfwCheck(@RequestBody GetNsfwResultInput input, @ApiParam(hidden = true) Session session, @ApiParam(hidden = true) HttpServletRequest request) { + //对用户做限流、鉴权的一系列操作 + boolean bl = limitUtils.limitCheckByBizAndUserId(session.getUserId(), "nsfw", 200); + ToastResultCode.SYS_OPERATOR_24_HOUR_ERROR.check(bl); + S3BucketNameEnum s3BucketNameEnum = input.getBizTypeEnum().getS3BucketNameEnum(); + //6.14.0 操作权限判断 + optAccessCheck(session.getUserId(), input.getBizTypeEnum(), input.getFileFullPath()); + HeadObjectResponse objectHeader = awsS3Utils.getObjectHeader(s3BucketNameEnum, input.getFileFullPath()); + ToastResultCode.FILE_NOT_EXISTS_ERROR.check(objectHeader == null); + ToastResultCode.FILE_MAX_SIZE_ERROR.check(objectHeader.contentLength() != null && objectHeader.contentLength() > Constants.FILE_MAX_BYTE); + ToastResultCode.FILE_TYPE_ERROR.check(!Constants.FILE_TYPE_LIST.contains(objectHeader.contentType())); + + //图片鉴黄操作 + NsfwRecognitionResult nsfwRecognitionResult = nsfwConfigService.nsfwRecognition(s3BucketNameEnum, input.getFileFullPath()); + //鉴黄不通过将图片删除掉 + if (nsfwRecognitionResult.getBl() != null && nsfwRecognitionResult.getBl()) { + //先不删除图片,将被鉴黄的图片数据给存储起来(后面用定时任务去删除) + nsfwLogService.add(session.getUserId(), s3BucketNameEnum, input.getFileFullPath(), nsfwRecognitionResult); + BizResultCode.BIZ_NSFW_VALIDATE_ERROR.check(true); + } + //将鉴黄结果存储在缓存中 + imageCheckService.addImageCheckResult(s3BucketNameEnum.getRootUrl() + input.getFileFullPath()); + return Result.success(nsfwRecognitionResult.getBl()); + } + + + /** + * 操作权限校验 + * @param currentUserId + * @param s3BizTypeMappingEnum + * @param filePath + */ + private void optAccessCheck(Long currentUserId, S3BizTypeMappingEnum s3BizTypeMappingEnum, String filePath) { + //获取到根路径 + String homePath = s3BizTypeMappingEnum.getFilePath(); + //获取需要移除掉的根路径的长度 + int removeIndex = filePath.indexOf(homePath) + homePath.length(); + log.info("===> optAccessCheck removeIndex : {}", removeIndex); + //移除掉根路径 + String removePath = filePath.substring(removeIndex); + log.info("===> optAccessCheck removePath : {}", removePath); + //从移除掉根路径的文件路径中获取第一个分隔符 + int userIdIndex = removePath.indexOf("/"); + String userId = removePath.substring(0, userIdIndex); + log.info("===> optAccessCheck userId : {}", userId); + //校验当前操作用户和url连接中的用户是否匹配,如果不匹配则直接抛出异常 + ToastResultCode.NO_OPERATION_AUTH.check(!currentUserId.toString().equals(userId)); + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/dao/BlurryImgRecordDao.java b/sonic-shark/server/src/main/java/com/sonic/shark/dao/BlurryImgRecordDao.java new file mode 100644 index 0000000..9c5e0e5 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/dao/BlurryImgRecordDao.java @@ -0,0 +1,11 @@ +package com.sonic.shark.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.shark.domain.entity.BlurryImgRecord; + +/** + * 模糊图片访问地址生成记录表 + * @author code + */ +public interface BlurryImgRecordDao extends BaseMapper { +} \ No newline at end of file diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/dao/FileUploadLogDao.java b/sonic-shark/server/src/main/java/com/sonic/shark/dao/FileUploadLogDao.java new file mode 100644 index 0000000..773789f --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/dao/FileUploadLogDao.java @@ -0,0 +1,12 @@ +package com.sonic.shark.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.shark.domain.entity.FileUploadLog; + +/** + * @author code + */ +public interface FileUploadLogDao extends BaseMapper { + + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/dao/NsfwConfigDao.java b/sonic-shark/server/src/main/java/com/sonic/shark/dao/NsfwConfigDao.java new file mode 100644 index 0000000..28e9d09 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/dao/NsfwConfigDao.java @@ -0,0 +1,12 @@ +package com.sonic.shark.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.shark.domain.entity.NsfwConfig; + +/** + * @author code + */ +public interface NsfwConfigDao extends BaseMapper { + + +} \ No newline at end of file diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/dao/NsfwLogDao.java b/sonic-shark/server/src/main/java/com/sonic/shark/dao/NsfwLogDao.java new file mode 100644 index 0000000..1a5e4bd --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/dao/NsfwLogDao.java @@ -0,0 +1,13 @@ +package com.sonic.shark.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sonic.shark.domain.entity.NsfwLog; + +/** + * 图鉴鉴黄日志表 + * @author code + */ +public interface NsfwLogDao extends BaseMapper { + + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/FaceFeaturesBo.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/FaceFeaturesBo.java new file mode 100644 index 0000000..46d58f6 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/FaceFeaturesBo.java @@ -0,0 +1,26 @@ +package com.sonic.shark.domain.bo; + +import lombok.Data; + +/** + * @Author code + */ +@Data +public class FaceFeaturesBo { + + /** + * 人脸年龄 + */ + private Integer age; + + /** + * 人脸性别 + */ + private Integer sex; + + /** + * 性别的置信度分数 + */ + private Double sexConfidence; + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/GetFaceLivenessSessionBo.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/GetFaceLivenessSessionBo.java new file mode 100644 index 0000000..2d64e1d --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/GetFaceLivenessSessionBo.java @@ -0,0 +1,26 @@ +package com.sonic.shark.domain.bo; + +import lombok.Data; + +/** + * @Author code + */ +@Data +public class GetFaceLivenessSessionBo { + + /** + * 状态,目前所知的值有 SUCCEEDED、EXPIRED 所以在业务上使用时只要不是SUCCEEDED都认为是异常 + */ + private String status; + + /** + * 文件的具体路径 + */ + private String path; + + /** + * 置信度分数 + */ + private Float confidence; + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/NsfwRecognitionResult.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/NsfwRecognitionResult.java new file mode 100644 index 0000000..4ba6c86 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/NsfwRecognitionResult.java @@ -0,0 +1,26 @@ +package com.sonic.shark.domain.bo; + +import com.amazonaws.services.rekognition.model.ModerationLabel; +import lombok.Data; + +import java.util.List; + +/** + * @Author code + * @Date 2022/4/18 + * @Version 1.0 + */ +@Data +public class NsfwRecognitionResult { + + /** + * 鉴黄结果 + */ + Boolean bl; + + /** + * 标签列表 + */ + List labelList; + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/S3StsToken.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/S3StsToken.java new file mode 100644 index 0000000..86c09ac --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/bo/S3StsToken.java @@ -0,0 +1,39 @@ +package com.sonic.shark.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: 获取oss的sts授权token访问权限 + * @author: code + **/ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class S3StsToken { + + private String expiration; + + private String accessKeyId; + + private String accessKeySecret; + + private String securityToken; + + private String requestId; + + private String bucket; + + private String endPoint; + + private String region; + + private String path; + + private String fileName; + + private String urlPath; +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/BlurryImgRecord.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/BlurryImgRecord.java new file mode 100644 index 0000000..3cdd295 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/BlurryImgRecord.java @@ -0,0 +1,75 @@ +package com.sonic.shark.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 模糊图片访问地址生成记录表 + * @author code + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "t_blurry_img_record", autoResultMap = true) +public class BlurryImgRecord { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 业务类型(ALBUM 相册) + */ + private String bizType; + + /** + * 业务ID + */ + private Long bizId; + + /** + * 原始图片的相对路径 + */ + private String sourceImgPath; + + /** + * 图片的相对路径 + */ + private String imgPath; + + /** + * 模糊图片地址全路径(尺寸:468*600) + */ + private String img1; + + /** + * 模糊图片地址全路径(尺寸:800*800) + */ + private String img2; + + /** + * 模糊图片地址全路径(原图) + */ + private String img3; + + /** + * 创建人 + */ + private Long creatorId; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/FileUploadLog.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/FileUploadLog.java new file mode 100644 index 0000000..6b3df97 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/FileUploadLog.java @@ -0,0 +1,93 @@ +package com.sonic.shark.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 业务文件上传持久化保存记录表 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "t_file_upload_log", autoResultMap = true) +public class FileUploadLog { + + @TableId(type = IdType.AUTO) + /** + * 主键id + */ + private Long id; + + /** + * bucketname名称 + */ + private String bucketName; + + /** + * 文件相对路径 + */ + private String filePath; + + /** + * 文件全路径(包含host可直接访问) + */ + private String fullPath; + + /** + * 是否鉴黄通过(0 否、1 是、空代表未鉴黄) + */ + private Boolean nsfwStatus; + + /** + * 鉴黄的结果内容 + */ + private String nsfwContent; + + /** + * IP地址 + */ + private String ipAddr; + + /** + * 端点来源 + */ + private String endpoint; + + /** + * 业务确认状态(0 false未确认、1 true 已经确认) + */ + private Boolean bizConfirmStatus; + + /** + * 是否删除(0 否 、1 是) + */ + private Boolean isDelete; + + /** + * 创建人id + */ + private Long creatorId; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 编辑人 + */ + private Long editorId; + + /** + * 编辑时间 + */ + private LocalDateTime editTime; +} \ No newline at end of file diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/NsfwConfig.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/NsfwConfig.java new file mode 100644 index 0000000..9574403 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/NsfwConfig.java @@ -0,0 +1,74 @@ +package com.sonic.shark.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "t_nsfw_config", autoResultMap = true) +public class NsfwConfig { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 类别 + */ + private String category; + + /** + * 分类等级(1/2/3) + */ + private String taxonomyLevel; + + /** + * 类别的中文描述 + */ + private String categoryDesc; + + /** + * 得分判断的阀值 + */ + private Float score; + + /** + * 父级ID + */ + private Long parentId; + + /** + * 父级类别 + */ + private String parentCategory; + + /** + * 父级类别的中文描述 + */ + private String parentCategoryDesc; + + /** + * 版本类型(空或DEFAULT为默认类型、AI 为AI图片) + */ + private String versionType; + + + /** + * 版本类型 + */ + public enum VersionType { + DEFAULT, + AI + ; + } + +} \ No newline at end of file diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/NsfwLog.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/NsfwLog.java new file mode 100644 index 0000000..453c682 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/entity/NsfwLog.java @@ -0,0 +1,64 @@ +package com.sonic.shark.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 图鉴鉴黄日志表 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "t_nsfw_log", autoResultMap = true) +public class NsfwLog { + + @TableId(type = IdType.AUTO) + /** + * 主键id + */ + private Long id; + + /** + * 用户id + */ + private Long userId; + + /** + * 存储桶的名称 + */ + private String bucketName; + + /** + * 图片全路径 + */ + private String url; + + /** + * nsfw的标签 + */ + private String nsfwLabel; + + /** + * 日志id + */ + private String traceId; + + /** + * 是否删除 + */ + private Boolean isDelete; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/BatchCheckTextInput.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/BatchCheckTextInput.java new file mode 100644 index 0000000..81ff34d --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/BatchCheckTextInput.java @@ -0,0 +1,90 @@ +package com.sonic.shark.domain.input; + +import java.util.List; + +/** + * 校验文本内容是否敏感 + * + * @author mzc + */ +public class BatchCheckTextInput { + + /** 文本内容 */ + List contentList; + + /** + * 用户ID + */ + Long userId; + + /** + * 性别 + */ + Integer gender; + + /** + * IP地址 + */ + String ip; + + /** + * 设备号 + */ + String deviceId; + + /** + * 业务类型(例:发帖、修改帖子 等) + */ + String bizType; + + public BatchCheckTextInput() { + } + + public List getContentList() { + return contentList; + } + + public void setContentList(List contentList) { + this.contentList = contentList; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Integer getGender() { + return gender; + } + + public void setGender(Integer gender) { + this.gender = gender; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getBizType() { + return bizType; + } + + public void setBizType(String bizType) { + this.bizType = bizType; + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/CheckImageInput.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/CheckImageInput.java new file mode 100644 index 0000000..9276fcc --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/CheckImageInput.java @@ -0,0 +1,27 @@ +package com.sonic.shark.domain.input; + +/** + * 校验图片是否敏感 + * + * @author mzc + */ +public class CheckImageInput { + + /** 图片链接地址 */ + String imageUrl; + + public CheckImageInput() { + } + + public CheckImageInput(String imageUrl) { + this.imageUrl = imageUrl; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/CheckTextInput.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/CheckTextInput.java new file mode 100644 index 0000000..8533317 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/CheckTextInput.java @@ -0,0 +1,92 @@ +package com.sonic.shark.domain.input; + +/** + * 校验文本内容是否敏感 + * + * @author mzc + */ +public class CheckTextInput { + + /** 文本内容 */ + String content; + + /** + * 用户ID + */ + Long userId; + + /** + * 性别 + */ + Integer gender; + + /** + * IP地址 + */ + String ip; + + /** + * 设备号 + */ + String deviceId; + + /** + * 业务类型(例:发帖、修改帖子 等) + */ + String bizType; + + public CheckTextInput() { + } + + public CheckTextInput(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Integer getGender() { + return gender; + } + + public void setGender(Integer gender) { + this.gender = gender; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getBizType() { + return bizType; + } + + public void setBizType(String bizType) { + this.bizType = bizType; + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/GetNsfwResultInput.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/GetNsfwResultInput.java new file mode 100644 index 0000000..7621926 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/GetNsfwResultInput.java @@ -0,0 +1,25 @@ +package com.sonic.shark.domain.input; + +import com.sonic.shark.enums.S3BizTypeMappingEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 获取文件的鉴黄结果入参 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GetNsfwResultInput { + + @ApiModelProperty("业务枚举类型") + private S3BizTypeMappingEnum bizTypeEnum; + + @ApiModelProperty("文件路径") + private String fileFullPath; + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/ImageCompressInput.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/ImageCompressInput.java new file mode 100644 index 0000000..db7931d --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/ImageCompressInput.java @@ -0,0 +1,24 @@ +package com.sonic.shark.domain.input; + +import io.swagger.annotations.ApiModelProperty; + +import java.util.List; + +/** + * @description: + * @author: mzc + * @date: 2021-01-08 11:32 + **/ +public class ImageCompressInput { + + @ApiModelProperty("图片地址") + private List imgUrl; + + public List getImgUrl() { + return imgUrl; + } + + public void setImgUrl(List imgUrl) { + this.imgUrl = imgUrl; + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/NsfwRecognitionInput.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/NsfwRecognitionInput.java new file mode 100644 index 0000000..71555b5 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/NsfwRecognitionInput.java @@ -0,0 +1,25 @@ +package com.sonic.shark.domain.input; + +import com.sonic.shark.enums.S3BizTypeMappingEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 获取文件的鉴黄结果入参 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NsfwRecognitionInput { + + @ApiModelProperty("业务枚举类型") + private S3BizTypeMappingEnum bizTypeEnum; + + @ApiModelProperty("文件路径") + private String fileFullPath; + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/StsTokenInput.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/StsTokenInput.java new file mode 100644 index 0000000..36eaf75 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/StsTokenInput.java @@ -0,0 +1,25 @@ +package com.sonic.shark.domain.input; + +import com.sonic.shark.enums.S3BizTypeMappingEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StsTokenInput { + + /** + * 业务类型 + */ + private S3BizTypeMappingEnum bizTypeEnum; + + /** + * 文件后缀 + */ + private String suffix; + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/UploadAwsS3Input.java b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/UploadAwsS3Input.java new file mode 100644 index 0000000..44dee35 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/domain/input/UploadAwsS3Input.java @@ -0,0 +1,20 @@ +package com.sonic.shark.domain.input; + +import com.sonic.shark.enums.S3BizTypeMappingEnum; +import lombok.Data; + +/** + * @Author code + */ +@Data +public class UploadAwsS3Input { + + private byte[] bytes; + + private S3BizTypeMappingEnum bizTypeEnum; + + /** + * 文件后缀 jpg,mp3 + */ + private String suffix; +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/enums/BizResultCode.java b/sonic-shark/server/src/main/java/com/sonic/shark/enums/BizResultCode.java new file mode 100644 index 0000000..db7364b --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/enums/BizResultCode.java @@ -0,0 +1,75 @@ +package com.sonic.shark.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum BizResultCode implements ApiResultCode { + + /** + * 鉴黄不通过 + */ + BIZ_NSFW_VALIDATE_ERROR("9999", "NSFW materials have been detected, please upload other photos"), + + MISS_PARAM_ERROR("1001010", "Missing parameter"), + + ; + + + private final String errorCode; + private final String errorMsg; + + BizResultCode(String errorCode, String errorMsg) { + this.errorCode = getAppId() + errorCode; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1001"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + + /** + * 校验方法 + * @param expect + * @param code + * @param message + */ + public static void check(boolean expect, String code, String message) { + if (expect) { + throw new BizException(code, message); + } + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/enums/BusinessException.java b/sonic-shark/server/src/main/java/com/sonic/shark/enums/BusinessException.java new file mode 100644 index 0000000..543d191 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/enums/BusinessException.java @@ -0,0 +1,39 @@ +package com.sonic.shark.enums; + + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.GlobalResultCode; + +public class BusinessException extends BizException { + + private static final long serialVersionUID = -5317007026578376164L; + + /** + * 错误码 + */ + private String errorCode; + /** + * 错误描述 + */ + private String errorMsg; + + /** + * @param errorCode + * @param errorMsg + */ + public BusinessException(String errorCode, String errorMsg) { + super(GlobalResultCode.INVALID_PARAMS.getErrorCode(), String.format("BusinessException{errorCode:%s, errorMsg:%s}", errorCode, errorMsg)); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + @Override + public String getErrorCode() { + return errorCode; + } + + @Override + public String getErrorMsg() { + return errorMsg; + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/enums/ContentTypeEnum.java b/sonic-shark/server/src/main/java/com/sonic/shark/enums/ContentTypeEnum.java new file mode 100644 index 0000000..86db2c6 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/enums/ContentTypeEnum.java @@ -0,0 +1,86 @@ +package com.sonic.shark.enums; + +import org.apache.commons.lang3.StringUtils; + +/** + * @description: 文件类型枚举类 + * @author: code + **/ +public enum ContentTypeEnum { + + BMP(".bmp", "image/bmp"), + GIF(".gif", "image/gif"), + JPEG(".jpeg", "image/jpeg"), + JPG(".jpg", "image/jpeg"), + PNG(".png", "image/jpeg"), + HTML(".html", "text/html"), + TXT(".txt", "text/plain"), + VSD(".vsd", "application/vnd.visio"), + PPTX(".pptx", "application/vnd.ms-powerpoint"), + PPT(".ppt", "application/vnd.ms-powerpoint"), + DOCX(".docx", "application/msword"), + DOC(".doc", "application/msword"), + XLSX(".xlsx", "application/vnd.ms-excel"), + XLS(".xls", "application/vnd.ms-excel"), + CSV(".csv", "text/csv"), + XML(".xml", "text/xml"), + PDF(".pdf", "application/pdf"), + APK(".apk", "application/vnd.android.package-archive"), + IPA(".ipa", "application/vnd.iphone"), + UNKNOWN(".", "application/octet-stream"), + + MP4(".mp4", "video/mp4"), + GP3(".3gp", "video/3gpp"), + AVI(".avi", "video/x-msvideo"), + WMV(".wmv", "video/x-ms-wmv"), + RM(".rm", "application/vnd.rn-realmedia"), + SWF(".swf", "application/x-shockwave-flash"), + MOV(".mov", "video/quicktime"), + M4V(".m4v", "video/x-m4v"), + FLV(".flv", "video/x-flv"), + DIF(".dif", "video/x-dv"), + DV(".dv", "video/x-dv"), + M4U(".m4u", "video/vnd.mpegurl"), + MOVIE(".movie", "video/x-sgi-movie"), + MPE(".mpe", "video/mpeg"), + MPEG(".mpeg", "video/mpeg"), + MPG(".mpg", "video/mpeg"), + MXU(".mxu", "video/vnd.mpegurl"), + OGV(".ogv", "video/ogv"), + QT(".qt", "video/quicktime"), + WEBM(".webm", "video/webm"), + MP3(".mp3", "audio/mpeg"), + + + ; + + private String suffix; + + private String type; + + ContentTypeEnum(String suffix, String type) { + this.suffix = suffix; + this.type = type; + } + + public String getSuffix() { + return suffix; + } + + public String getType() { + return type; + } + + public static String getContentType(String suffix) { + if(StringUtils.isEmpty(suffix)) { + return null; + } + for (ContentTypeEnum contentTypeEnum: ContentTypeEnum.values()) { + if(StringUtils.equalsIgnoreCase(suffix, contentTypeEnum.suffix)) { + return contentTypeEnum.type; + } + } + return null; + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/enums/S3BizTypeMappingEnum.java b/sonic-shark/server/src/main/java/com/sonic/shark/enums/S3BizTypeMappingEnum.java new file mode 100644 index 0000000..8d23745 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/enums/S3BizTypeMappingEnum.java @@ -0,0 +1,66 @@ +package com.sonic.shark.enums; + +import lombok.Getter; + +/** + * 业务类型映射bucket关系 + * @Author code + */ +@Getter +public enum S3BizTypeMappingEnum { + + /** + * 头像 + */ + HEAD_IMAGE(S3BucketNameEnum.SONIC_SHARK, "headImage/", true), + + /** + * 头像、相册图片上传 + */ + ALBUM(S3BucketNameEnum.SONIC_SHARK, "album/", true), + + /** + * 角色形象图 + */ + ROLE(S3BucketNameEnum.SONIC_SHARK, "role/", false), + + /** + * 声音 + */ + SOUND(S3BucketNameEnum.SONIC_SOUND, "sound/", false), + + /** + * 声音 + */ + SOUND_PATH(S3BucketNameEnum.SONIC_SOUND, "sound/", false), + + /** + * IM相关 + */ + IM_IMG(S3BucketNameEnum.SONIC_IM, "im/img/", false), + + ; + + + /** + * 映射的S3存储桶 + */ + private S3BucketNameEnum s3BucketNameEnum; + + /** + * 文件的存储路径 + */ + private String filePath; + + /** + * 是否需要强制用户登录校验 + */ + private Boolean loginCheck; + + + S3BizTypeMappingEnum(S3BucketNameEnum s3BucketNameEnum, String filePath, Boolean loginCheck) { + this.s3BucketNameEnum = s3BucketNameEnum; + this.filePath = filePath; + this.loginCheck = loginCheck; + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/enums/S3BucketNameEnum.java b/sonic-shark/server/src/main/java/com/sonic/shark/enums/S3BucketNameEnum.java new file mode 100644 index 0000000..a3696df --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/enums/S3BucketNameEnum.java @@ -0,0 +1,74 @@ +package com.sonic.shark.enums; + + +import com.amazonaws.regions.Regions; +import lombok.Getter; +import software.amazon.awssdk.regions.Region; + +/** + * S3 bucketName 枚举 + **/ +@Getter +public enum S3BucketNameEnum { + + /** + * 相册 + */ + SONIC_SHARK(1, "crushthem1", "prod/", true, Regions.US_WEST_2, Region.US_WEST_2, "crushthem1.s3.us-west-2.amazonaws.com", "https://hhb.crushlevel.ai/"), + + /** + * 存储模糊图片 + */ + BLURRY(2, "cl-sub", "prod/", false, Regions.US_WEST_2, Region.US_WEST_2, "cl-sub.s3.us-west-2.amazonaws.com", "https://sub.crushlevel.ai/"), + + /** + * 声音存储桶 + */ + SONIC_SOUND(3, "cl-sound", "prod/", true, Regions.US_WEST_2, Region.US_WEST_2, "cl-sound.s3.us-west-2.amazonaws.com", "https://snd.crushlevel.ai/"), + + /** + * IM存储桶 + */ + SONIC_IM(4, "cl-im", "prod/", true, Regions.US_WEST_2, Region.US_WEST_2, "cl-im.s3.us-west-2.amazonaws.com", "https://img.crushlevel.ai/"), + + ; + + /** + * 状态编码 + */ + private int index; + + private String bucketName; + + private String prefix; + + /** + * 是否可以授权拿到 s3 的 sts token 进行 + */ + private Boolean s3StsTokenBl; + + /** + * 图像识别时使用的区域 + */ + private Regions regions; + + /** + * S3文件上传时使用的区域 + */ + private Region region; + + private String endPoint; + + private String rootUrl; + + S3BucketNameEnum(int index, String bucketName, String prefix, Boolean s3StsTokenBl, Regions regions, Region region, String endPoint, String rootUrl) { + this.index = index; + this.bucketName = bucketName; + this.prefix = prefix; + this.s3StsTokenBl = s3StsTokenBl; + this.regions = regions; + this.region = region; + this.endPoint = endPoint; + this.rootUrl = rootUrl; + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/enums/ToastResultCode.java b/sonic-shark/server/src/main/java/com/sonic/shark/enums/ToastResultCode.java new file mode 100644 index 0000000..efcc36e --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/enums/ToastResultCode.java @@ -0,0 +1,91 @@ +package com.sonic.shark.enums; + +import com.sonic.common.exception.BizException; +import com.sonic.common.rpc.ApiResultCode; +import com.sonic.common.utils.MessageUtils; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +/** + * @description: 弹窗消息的定义 + * @author: code + * @create: 2021-12-17 10:25 + **/ +@Getter +public enum ToastResultCode implements ApiResultCode { + NO_OPERATION_AUTH("No operation authority"), + + NETWORK_FAILURE("Network failure"), + SYSTEM_EXCEPTION("Internal error, please try again later"), + + SYS_OPERATOR_24_HOUR_ERROR("operate too frequently,please try again 24h later"), + + SYS_BIZ_UPGRADE_ERROR("Feature adjusted, check for update in the App/Play store"), + + + /** + * 文件上传相关 + */ + PICTURES_VALIDATE_ERROR("NSFW materials have been detected, please upload other photos"), + + /** + * 图片鉴黄 + */ + FILE_NOT_EXISTS_ERROR("File does not exist"), + FILE_MAX_SIZE_ERROR("File exceeds maximum limit"), + FILE_TYPE_ERROR("File type does not exist"), + INVALID_REQUEST_IMAGE_ADDRESS("Invalid request image address"), + STS_BIZ_TYPE_ERROR("wrong business type"), + + ; + + + private final String errorCode; + private final String errorMsg; + + ToastResultCode(String errorMsg) { + this.errorCode = ""; + this.errorMsg = errorMsg; + } + + /** + * 得到 message 的值. + * + * @return 属性 message 的值. + */ + @Override + public String getErrorMsg() { + return errorMsg + "(" + getAppId() + ")"; + } + + /** + * @return 当前服务的AppId + */ + @Override + public String getAppId() { + return "1002"; + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect) { + if (expect) { + String message = MessageUtils.get(this.name()); + throw new BizException(this.getErrorCode(), this.name().equals(message) ? this.getErrorMsg() : message); + } + } + + /** + * 校验方法 + * + * @param expect + */ + public void check(boolean expect, String msg) { + if (expect) { + throw new BizException(this.getErrorCode(), StringUtils.isNotEmpty(msg) ? msg : this.getErrorMsg()); + } + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/event/inner/EventType.java b/sonic-shark/server/src/main/java/com/sonic/shark/event/inner/EventType.java new file mode 100644 index 0000000..185d912 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/event/inner/EventType.java @@ -0,0 +1,30 @@ +package com.sonic.shark.event.inner; + +import com.sonic.shark.config.EventConfig; +import com.sonic.common.event.Event; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义当前系统的消息 + * @author code + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** 事件定义 */ + DEMO_CREATED(EventConfig.DEFAULT_SCENE, EventConfig.DEFAULT_MODULE, "demo_created", "demo 创建"), + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/event/inner/handler/DemoCreatedThenHandler.java b/sonic-shark/server/src/main/java/com/sonic/shark/event/inner/handler/DemoCreatedThenHandler.java new file mode 100644 index 0000000..a87f537 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/event/inner/handler/DemoCreatedThenHandler.java @@ -0,0 +1,33 @@ +package com.sonic.shark.event.inner.handler; + +import com.sonic.shark.event.inner.EventType; +import com.sonic.shark.event.inner.payload.DemoCreatedPayload; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author code + */ +@Slf4j +@Component +public class DemoCreatedThenHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + + @Override + public void onEvent(Event event) { + DemoCreatedPayload payload = event.normalizedData(DemoCreatedPayload.class); + // TODO: + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.DEMO_CREATED.getEventCode(), this); + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/event/inner/payload/DemoCreatedPayload.java b/sonic-shark/server/src/main/java/com/sonic/shark/event/inner/payload/DemoCreatedPayload.java new file mode 100644 index 0000000..db79764 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/event/inner/payload/DemoCreatedPayload.java @@ -0,0 +1,18 @@ +package com.sonic.shark.event.inner.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author code + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DemoCreatedPayload { + private Long demoId; +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/event/outer/EventType.java b/sonic-shark/server/src/main/java/com/sonic/shark/event/outer/EventType.java new file mode 100644 index 0000000..24f1b84 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/event/outer/EventType.java @@ -0,0 +1,29 @@ +package com.sonic.shark.event.outer; + +import com.sonic.common.event.Event; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 定义不属于当前系统的消息 + * @author code + */ +@AllArgsConstructor +@Getter +public enum EventType { + /** 事件定义 */ + USER_CREATED(Event.BuildInScene.BS.getCode(), "bs_user", "user_created", "用户创建"), + ; + + EventType(String scene, String module, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .scene(scene) + .module(module) + .name(name) + .build(); + this.desc = desc; + } + + private String desc; + private Event.EventCode eventCode; +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/event/outer/handler/UserCreatedThenHandler.java b/sonic-shark/server/src/main/java/com/sonic/shark/event/outer/handler/UserCreatedThenHandler.java new file mode 100644 index 0000000..d0e46dc --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/event/outer/handler/UserCreatedThenHandler.java @@ -0,0 +1,33 @@ +package com.sonic.shark.event.outer.handler; + +import com.sonic.shark.event.outer.EventType; +import com.sonic.shark.event.outer.payload.UserCratedPayload; +import com.sonic.common.event.Event; +import com.sonic.common.event.EventConsumer; +import com.sonic.common.event.EventHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author code + */ +@Slf4j +@Component +public class UserCreatedThenHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + + @Override + public void onEvent(Event event) { + UserCratedPayload payload = event.normalizedData(UserCratedPayload.class); + // TODO: + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandler(EventType.USER_CREATED.getEventCode(), this); + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/event/outer/payload/UserCratedPayload.java b/sonic-shark/server/src/main/java/com/sonic/shark/event/outer/payload/UserCratedPayload.java new file mode 100644 index 0000000..b01468d --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/event/outer/payload/UserCratedPayload.java @@ -0,0 +1,18 @@ +package com.sonic.shark.event.outer.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author code + * @date 2020-05-07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserCratedPayload { + private Long userId; +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/limit/RequestLimit.java b/sonic-shark/server/src/main/java/com/sonic/shark/limit/RequestLimit.java new file mode 100644 index 0000000..c89f404 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/limit/RequestLimit.java @@ -0,0 +1,45 @@ +package com.sonic.shark.limit; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.*; + +/** + * @description: 限流注解 + * @author: code + **/ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +@Order(Ordered.HIGHEST_PRECEDENCE) +public @interface RequestLimit { + + /** + * 允许访问的最大次数 + */ + int count() default Integer.MAX_VALUE; + + /** + * 已登录用户允许访问的最大次数(备注:如果不配置值的话默认都走IP的限制) + * @return + */ + int loginCount() default Integer.MAX_VALUE; + + /** + * 时间段,单位为毫秒,默认值一分钟 + */ + long time() default 60000; + + /** + * 未登录请求用户达到限流时的提示 异常码 默认值为:1001009 / 如果想要未登录用户跳转到登录页面则返回异常码为 noLoginErrorCode = "10050001" + * @return + */ + String noLoginErrorCode() default "1001009"; + + /** + * message 提示文案 + * @return + */ + String message() default "Sorry to detect your abnormal access, please try again later"; +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/limit/RequestLimitContract.java b/sonic-shark/server/src/main/java/com/sonic/shark/limit/RequestLimitContract.java new file mode 100644 index 0000000..d1e33de --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/limit/RequestLimitContract.java @@ -0,0 +1,109 @@ +package com.sonic.shark.limit; + + +import com.sonic.common.auth.domains.Session; +import com.sonic.common.enums.AppEnv; +import com.sonic.common.utils.IpAddressUtils; +import com.sonic.shark.enums.BizResultCode; +import com.sonic.shark.enums.BusinessException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 限流控制器 + */ +@Order(99) +@Slf4j +@Aspect +@Component +public class RequestLimitContract { + + @Value("${spring.profiles.active}") + private String runMode; + @Autowired + private StringRedisTemplate stringRedisTemplate; + + /** + * redis key不存在的过期时间值 + */ + private final Integer NULL_KEY_EXP_TIME_VALUE = -2; + + /** + * redis key永不过期的过期时间值 + */ + private final Integer NEVER_EXPIRES_VALUE = -1; + + /** + * 默认的异常码 + */ + private static final String DEFAULT_ERROR_CODE = "1999999"; + + @Before("execution(public * com.sonic.shark.controller..*.*(..)) && @annotation(limit)") + public void requestLimit(final JoinPoint joinPoint, RequestLimit limit) { + //dev环境直接放行不做拦截 + if (StringUtils.isNotBlank(runMode) && AppEnv.dev.name().equals(runMode)) { + return; + } + Object[] args = joinPoint.getArgs(); + HttpServletRequest request = null; + Session session = null; + for (Object arg : args) { + //解析方法的HttpServletRequest入参对象 + if (arg instanceof HttpServletRequest) { + request = (HttpServletRequest) arg; + } + //解析方法的Session入参对象、必须要配置了登录次数限制的才能进行解析 + if (arg instanceof Session && limit.loginCount() != Integer.MAX_VALUE) { + session = (Session) arg; + } + } + //判断请求是否为空,是否需要抛出异常 + BizResultCode.MISS_PARAM_ERROR.check(request == null); + int num = 0; + String ipOrUserId = null; + Integer limitCount = null; + String url = null; + boolean loginUserBl = false; + try { + ipOrUserId = (session == null || session.getUserId() == null) ? IpAddressUtils.getIpAddress(request) : session.getUserId().toString(); + limitCount = (session == null || session.getUserId() == null) ? limit.count() : limit.loginCount(); + loginUserBl = session != null && session.getUserId() != null; + url = request.getRequestURI(); + //eg: limit:path:/mobile/third/login:127-0-0-1 + String key = "limit:path:".concat(url).concat(":").concat(StringUtils.isNotEmpty(ipOrUserId) ? ipOrUserId.replace(":", "-") : ipOrUserId); + //处理限流为-1的情况 + Long expTime = stringRedisTemplate.getExpire(key); + if (expTime.intValue() == NULL_KEY_EXP_TIME_VALUE) { + num = 1; + stringRedisTemplate.opsForValue().set(key, "1", limit.time(), TimeUnit.MILLISECONDS); + } else { + num = Objects.requireNonNull(stringRedisTemplate.opsForValue().increment(key, 1)).intValue(); + } + //处理获取过期时间如果为-1的话直接将限流redis删掉 + if(expTime.intValue() == NEVER_EXPIRES_VALUE) { + //删除redis的key + stringRedisTemplate.delete(key); + } + } catch (Exception e) { + log.error("requestLimit error", e); + } + if (num > limitCount) { + log.info("===> 限流触发,访问地址:{}, 用户信息:{}, 限定的次数:{}", ipOrUserId, url, limit.count()); + //未登录的用户达到限流时如果配置了跳转登录页的errorCode的话前端会直接去跳转登录页面 + throw new BusinessException(loginUserBl ? DEFAULT_ERROR_CODE : limit.noLoginErrorCode(), limit.message()); + } + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/BlurryImgRecordService.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/BlurryImgRecordService.java new file mode 100644 index 0000000..dd58ab1 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/BlurryImgRecordService.java @@ -0,0 +1,37 @@ +package com.sonic.shark.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.shark.domain.entity.BlurryImgRecord; +import com.sonic.shark.enums.S3BucketNameEnum; +import com.sonic.shark.lib.output.BlurryRecordOutput; + +import java.util.List; + +/** + * 模糊图片访问地址生成记录表 + * @author code + */ +public interface BlurryImgRecordService extends IService { + + /** + * 保存数据 + * @param s3BucketNameEnum + * @param sourceImgPath + * @param imgPath + * @param bizType + * @param bizId + * @param userId + * @return + * @throws Exception + */ + BlurryImgRecord saveRecord(S3BucketNameEnum s3BucketNameEnum, String sourceImgPath, String imgPath, String bizType, Long bizId, Long userId) throws Exception; + + /** + * 查询模糊图片数据 + * @param bizType + * @param bizIdList + * @param imgUrlBl + * @return + */ + List queryBlurryImgList(String bizType, List bizIdList, Boolean imgUrlBl); +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/CopyAndBlurryImgService.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/CopyAndBlurryImgService.java new file mode 100644 index 0000000..66f265c --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/CopyAndBlurryImgService.java @@ -0,0 +1,25 @@ +package com.sonic.shark.service; + + +import com.sonic.shark.lib.input.CopyAndBlurryImgInput; +import com.sonic.shark.lib.output.CopyAndBlurryImgOutput; + +import java.util.List; + +/** + * @Author code + * @Description 拷贝并且模糊图片 + * @Date 2023/8/24 17:33 + * @Version 1.0 + */ +public interface CopyAndBlurryImgService { + + /** + * 拷贝并生成模糊图片访问地址 + * @param inputList + * @return + * @throws Exception + */ + List copyAndBlurryImg(List inputList) throws Exception; + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/FileUploadLogService.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/FileUploadLogService.java new file mode 100644 index 0000000..3104dbb --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/FileUploadLogService.java @@ -0,0 +1,31 @@ +package com.sonic.shark.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.shark.domain.bo.S3StsToken; +import com.sonic.shark.domain.entity.FileUploadLog; + +import java.util.List; + + +/** + * 获取和业务相关的sts token数据 + * @author code + */ +public interface FileUploadLogService extends IService { + + /** + * 保存文件上传日志数据 + * @param userId + * @param s3StsToken + * @param ip + * @param endpoint + */ + void save(Long userId, S3StsToken s3StsToken, String ip, String endpoint); + + /** + * 确认图片 + * @param userId + * @param imageList + */ + void confirm(Long userId, List imageList); +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/ImageCheckService.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/ImageCheckService.java new file mode 100644 index 0000000..6ced4eb --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/ImageCheckService.java @@ -0,0 +1,54 @@ +package com.sonic.shark.service; + +import java.util.List; + +/** + * @Author code + * @Date 2021/1/25 13:43 + * @Version 1.0 + */ +public interface ImageCheckService { + + /** + * 添加校验结果到缓存列表中 + * @param url + */ + void addImageCheckResult(String url); + + /** + * 校验图片是否通过了校验 + * @param url + */ + void checkImageIsPassed(String url); + + /** + * 批量校验图片是否通过了校验 + * @param urls + */ + void checkImageIsPassed(List urls); + + /** + * 业务上确认图片 + * @param userId + * @param urls + */ + void confirmImage(Long userId, List urls); + + /** + * 校验真人 + * @param url + */ + void addHumanIdentificationCheckResult(String url); + + /** + * 校验真人图片是否通过了校验 + * @param url + */ + void checkHumanIdentificationIsPassed(String url); + + /** + * 校验真人图片是否通过了校验 + * @param urls + */ + void checkHumanIdentificationIsPassed(List urls); +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/NsfwConfigService.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/NsfwConfigService.java new file mode 100644 index 0000000..bb931e8 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/NsfwConfigService.java @@ -0,0 +1,52 @@ +package com.sonic.shark.service; + +import com.amazonaws.services.rekognition.model.ModerationLabel; +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.shark.domain.bo.NsfwRecognitionResult; +import com.sonic.shark.domain.entity.NsfwConfig; +import com.sonic.shark.enums.S3BucketNameEnum; + +import java.util.List; +import java.util.Map; + +/** + * @author code + */ +public interface NsfwConfigService extends IService { + + /** + * 图片鉴黄 + * @param s3BucketNameEnum + * @param fileFullPath + * @return + */ + NsfwRecognitionResult nsfwRecognition(S3BucketNameEnum s3BucketNameEnum, String fileFullPath); + + /** + * 图片鉴黄 + * @param s3BucketNameEnum + * @param fileFullPath + * @param versionType + * @return + */ + NsfwRecognitionResult nsfwRecognition(S3BucketNameEnum s3BucketNameEnum, String fileFullPath, String versionType); + + + /** + * 鉴黄校验 + * @param labelList + * @param versionType + * @return + */ + Boolean checkNsfw(List labelList, String versionType); + + + /** + * 根据类型列表获取类型对应的分值 + * @param categoryList + * @param versionType + * @return + */ + Map getScore(List categoryList, String versionType); + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/NsfwLogService.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/NsfwLogService.java new file mode 100644 index 0000000..017ee24 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/NsfwLogService.java @@ -0,0 +1,23 @@ +package com.sonic.shark.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.sonic.shark.domain.bo.NsfwRecognitionResult; +import com.sonic.shark.domain.entity.NsfwLog; +import com.sonic.shark.enums.S3BucketNameEnum; + +/** + * 图鉴鉴黄日志表 + * @author code + */ +public interface NsfwLogService extends IService { + + /** + * 添加数据 + * @param userId + * @param s3BucketNameEnum + * @param fileFullPath + * @param nsfwRecognitionResult + */ + void add(Long userId, S3BucketNameEnum s3BucketNameEnum, String fileFullPath, NsfwRecognitionResult nsfwRecognitionResult); + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/OssService.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/OssService.java new file mode 100644 index 0000000..110419c --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/OssService.java @@ -0,0 +1,45 @@ +package com.sonic.shark.service; + + +import com.sonic.shark.enums.S3BizTypeMappingEnum; +import com.sonic.shark.enums.S3BucketNameEnum; + +/** + * @description: oss文件上传 + * @author: code + **/ +public interface OssService { + + + /** + * 上传文件 + * + * @param bytes + * @param s3BizTypeMappingEnum + * @return + */ + String uploadFileBytes(byte[] bytes, S3BizTypeMappingEnum s3BizTypeMappingEnum, String suffix); + + /** + * 上传文件 + * + * @param bytes + * @param s3BucketNameEnum + * @param path + * @param fileName + * @param publicRead + * @return + */ + String uploadByBytes(byte[] bytes, S3BucketNameEnum s3BucketNameEnum, String path, String fileName, boolean publicRead); + + /** + * 复制文件(将源bucket中的文件复制到目标bucket中) + * + * @param sourceS3BucketNameEnum + * @param sourceFilePath + * @param targetS3BucketNameEnum + * @param targetFilePath + * @return + */ + String copyFile(S3BucketNameEnum sourceS3BucketNameEnum, String sourceFilePath, S3BucketNameEnum targetS3BucketNameEnum, String targetFilePath); +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/S3StsService.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/S3StsService.java new file mode 100644 index 0000000..66f261e --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/S3StsService.java @@ -0,0 +1,35 @@ +package com.sonic.shark.service; + +import com.sonic.shark.domain.bo.S3StsToken; +import com.sonic.shark.enums.S3BizTypeMappingEnum; +import com.sonic.shark.enums.S3BucketNameEnum; + +public interface S3StsService { + + /** + * 根据业务类型获取用户上传文件的 STS token 文件目录授权 + * @param s3BizTypeMappingEnum + * @param userId + * @param suffix + * @return + */ + S3StsToken getStsToken(S3BizTypeMappingEnum s3BizTypeMappingEnum, Long userId, String suffix); + + /** + * 获取STS token基础方法 到文件 + * @param bucketNameEnum + * @param path + * @param fileName + * @return + */ + S3StsToken getStsTokenPolicy(S3BucketNameEnum bucketNameEnum, String path, String fileName); + + /** + * 获取S3的STS授权token 底层方法 + * + * @param policy 授权策略 + * @return + */ + S3StsToken getStsToken(String region, String path, String policy); + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/BlurryImgRecordServiceImpl.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/BlurryImgRecordServiceImpl.java new file mode 100644 index 0000000..e1e567e --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/BlurryImgRecordServiceImpl.java @@ -0,0 +1,78 @@ +package com.sonic.shark.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.sonic.common.utils.CloudFrontSignerUtils; +import com.sonic.shark.dao.BlurryImgRecordDao; +import com.sonic.shark.domain.entity.BlurryImgRecord; +import com.sonic.shark.enums.S3BucketNameEnum; +import com.sonic.shark.lib.output.BlurryRecordOutput; +import com.sonic.shark.service.BlurryImgRecordService; +import com.sonic.shark.utils.BeanConvert; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +/** + * 模糊图片访问地址生成记录表 + * + * @author code + */ +@Slf4j +@Service +public class BlurryImgRecordServiceImpl extends ServiceImpl implements BlurryImgRecordService { + + private static final Long EXP_TIME_2037 = 2114227200000L; + + @Autowired + private CloudFrontSignerUtils cloudFrontSignerUtils; + + @Override + public BlurryImgRecord saveRecord(S3BucketNameEnum s3BucketNameEnum, String sourceImgPath, String imgPath, String bizType, Long bizId, Long userId) throws Exception { + BlurryImgRecord record = new BlurryImgRecord(); + record.setBizType(bizType); + record.setBizId(bizId); + record.setSourceImgPath(sourceImgPath); + record.setImgPath(imgPath); + //生成图片访问的签名链接 + String fileUrl = s3BucketNameEnum.getRootUrl() + imgPath; + String img1Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.BLURRY_IMG_468_600, new Date(EXP_TIME_2037), null); + String img2Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.BLURRY_IMG_800_800, new Date(EXP_TIME_2037), null); + String img3Url = cloudFrontSignerUtils.signer(fileUrl + CloudFrontSignerUtils.BLURRY_ORG_IMG, new Date(EXP_TIME_2037), null); + record.setImg1(img1Url); + record.setImg2(img2Url); + record.setImg3(img3Url); + record.setCreatorId(userId); + record.setCreateTime(LocalDateTime.now()); + save(record); + return record; + } + + @Override + public List queryBlurryImgList(String bizType, List bizIdList, Boolean imgUrlBl) { + if (StringUtils.isEmpty(bizType) || CollectionUtils.isEmpty(bizIdList)) { + return Lists.newArrayList(); + } + List list = list(Wrappers.lambdaQuery() + .select(BlurryImgRecord::getBizId, BlurryImgRecord::getImgPath, BlurryImgRecord::getImg1, BlurryImgRecord::getImg2, BlurryImgRecord::getImg3) + .eq(BlurryImgRecord::getBizType, bizType) + .in(BlurryImgRecord::getBizId, bizIdList)); + List outputList = BeanConvert.copeList(list, BlurryRecordOutput.class); + if(imgUrlBl != null && imgUrlBl) { + S3BucketNameEnum s3BucketNameEnum = S3BucketNameEnum.BLURRY; + for (BlurryRecordOutput output : outputList) { + output.setImgUrl(s3BucketNameEnum.getRootUrl() + output.getImgPath()); + } + } + return outputList; + } + + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/CopyAndBlurryImgServiceImpl.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/CopyAndBlurryImgServiceImpl.java new file mode 100644 index 0000000..4ccd4cc --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/CopyAndBlurryImgServiceImpl.java @@ -0,0 +1,77 @@ +package com.sonic.shark.service.impl; + +import com.google.common.collect.Lists; +import com.sonic.shark.domain.entity.BlurryImgRecord; +import com.sonic.shark.enums.S3BucketNameEnum; +import com.sonic.shark.lib.input.CopyAndBlurryImgInput; +import com.sonic.shark.lib.output.CopyAndBlurryImgOutput; +import com.sonic.shark.service.BlurryImgRecordService; +import com.sonic.shark.service.CopyAndBlurryImgService; +import com.sonic.shark.service.OssService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +/** + * @Author code + * @Description 拷贝并且模糊图片 + */ +@Slf4j +@Service +public class CopyAndBlurryImgServiceImpl implements CopyAndBlurryImgService { + + @Value("${spring.profiles.active}") + private String runMode; + + @Autowired + private OssService ossService; + + @Autowired + private BlurryImgRecordService blurryImgRecordService; + + /** + * 参考Onlyfans设置的目录路径 + */ + private static final String TARGET_PATH = "files/b/"; + + @Override + public List copyAndBlurryImg(List inputList) throws Exception { + S3BucketNameEnum s3BucketNameEnum = S3BucketNameEnum.BLURRY; + List outputList = Lists.newArrayList(); + //可以用异步线程池来处理并返回结果优化执行速度 + for (CopyAndBlurryImgInput input : inputList) { + //根据业务类型获取源图片存储的bucket + S3BucketNameEnum sourceS3BucketNameEnum = S3BucketNameEnum.SONIC_SHARK; + //处理图片的相对路径 + if(StringUtils.isNotEmpty(input.getSourceFileUrl())) { + //根据业务类型解析出相对路径 + input.setSourceFilePath(input.getSourceFileUrl().replace(sourceS3BucketNameEnum.getRootUrl(), "")); + } + //处理源文件的前缀多一个/的问题 + input.setSourceFilePath(input.getSourceFilePath().startsWith("/") ? input.getSourceFilePath().substring(1) : input.getSourceFilePath()); + //获取后缀 + String suffix = input.getSourceFilePath().substring(input.getSourceFilePath().lastIndexOf(".")); + //随机生成文件名称,用UUID吧 + String targetFilePath = TARGET_PATH + runMode + "/" + input.getUserId() + "/" + UUID.randomUUID().toString().replace("-", "") + suffix; + ossService.copyFile(sourceS3BucketNameEnum, input.getSourceFilePath(), s3BucketNameEnum, targetFilePath); + //保存数据 + BlurryImgRecord record = blurryImgRecordService.saveRecord(s3BucketNameEnum, input.getSourceFilePath(), targetFilePath, input.getBizType().name(), input.getBizId(), input.getUserId()); + CopyAndBlurryImgOutput output = new CopyAndBlurryImgOutput(); + output.setBizId(record.getBizId()); + output.setSourceImgPath(input.getSourceFilePath()); + output.setImgPath(record.getImgPath()); + output.setImg1(record.getImg1()); + output.setImg2(record.getImg2()); + output.setImg3(record.getImg3()); + output.setImgUrl(s3BucketNameEnum.getRootUrl() + record.getImgPath()); + outputList.add(output); + } + return outputList; + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/FileUploadLogServiceImpl.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/FileUploadLogServiceImpl.java new file mode 100644 index 0000000..c65bcec --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/FileUploadLogServiceImpl.java @@ -0,0 +1,52 @@ +package com.sonic.shark.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.shark.dao.FileUploadLogDao; +import com.sonic.shark.domain.bo.S3StsToken; +import com.sonic.shark.domain.entity.FileUploadLog; +import com.sonic.shark.service.FileUploadLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @Author code + */ +@Slf4j +@Service +public class FileUploadLogServiceImpl extends ServiceImpl implements FileUploadLogService { + + @Override + public void save(Long userId, S3StsToken s3StsToken, String ip, String endpoint) { + FileUploadLog log = FileUploadLog.builder() + .bucketName(s3StsToken.getBucket()) + .filePath(s3StsToken.getPath()) + .fullPath(s3StsToken.getUrlPath()) + .ipAddr(ip) + .endpoint(endpoint) + .bizConfirmStatus(false) + .isDelete(false) + .creatorId(userId) + .createTime(LocalDateTime.now()) + .editorId(userId) + .editTime(LocalDateTime.now()) + .build(); + save(log); + } + + @Override + public void confirm(Long userId, List imageList) { + if(CollectionUtils.isEmpty(imageList)) { + return; + } + update(Wrappers.lambdaUpdate() + .set(FileUploadLog::getBizConfirmStatus, true) + .set(userId != null, FileUploadLog::getEditorId, userId) + .eq(FileUploadLog::getBizConfirmStatus, false) + .in(FileUploadLog::getFullPath, imageList)); + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/ImageCheckServiceImpl.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/ImageCheckServiceImpl.java new file mode 100644 index 0000000..ba55866 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/ImageCheckServiceImpl.java @@ -0,0 +1,95 @@ +package com.sonic.shark.service.impl; + +import com.sonic.common.AppRuntime; +import com.sonic.common.exception.BizExceptionUtils; +import com.sonic.shark.enums.ToastResultCode; +import com.sonic.shark.service.FileUploadLogService; +import com.sonic.shark.service.ImageCheckService; +import com.sonic.shark.utils.MD5Util; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.sonic.shark.utils.Constants.DEFAULT_IMAGE_LIST; + +/** + * @Author code + * @Date 2021/1/25 13:43 + * @Version 1.0 + */ +@Slf4j +@Service +public class ImageCheckServiceImpl implements ImageCheckService { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Autowired + private AppRuntime appRuntime; + + @Autowired + private FileUploadLogService uploadLogService; + + + @Override + public void addImageCheckResult(String url) { + //将鉴黄校验通过的 数据保存到redis中 保存1个小时,1小时内提交的数据才是有效的,否则无效,不能上传图片上来 + String urlMd5 = MD5Util.digest(url); + String redisKey = appRuntime.buildPrefixKey("image", "check", urlMd5); + log.info("===> addImageCheckResult url : {}, redisKey : {}", url, redisKey); + stringRedisTemplate.opsForValue().set(redisKey, "1", 24, TimeUnit.HOURS); + } + + @Override + public void checkImageIsPassed(String url) { + if(DEFAULT_IMAGE_LIST.contains(url)) { + return; + } + String urlMd5 = MD5Util.digest(url); + String redisKey = appRuntime.buildPrefixKey("image", "check", urlMd5); + Boolean bl = stringRedisTemplate.hasKey(redisKey); + //无效的图片请求地址 + BizExceptionUtils.check(!bl, "", "Invalid request image address"); + } + + @Override + public void checkImageIsPassed(List urls) { + for (String url : urls) { + checkImageIsPassed(url); + } + //确认图片 + confirmImage(null, urls); + } + + @Override + public void confirmImage(Long userId, List urls) { + uploadLogService.confirm(userId, urls); + } + + @Override + public void addHumanIdentificationCheckResult(String url) { + String urlMd5 = MD5Util.digest(url); + String redisKey = appRuntime.buildPrefixKey("image","humanIdentification","check", urlMd5); + stringRedisTemplate.opsForValue().set(redisKey, "1", 24, TimeUnit.HOURS); + } + + @Override + public void checkHumanIdentificationIsPassed(String url) { + String urlMd5 = MD5Util.digest(url); + String redisKey = appRuntime.buildPrefixKey("image","humanIdentification","check", urlMd5); + Boolean bl = stringRedisTemplate.hasKey(redisKey); + //无效的图片请求地址 + ToastResultCode.INVALID_REQUEST_IMAGE_ADDRESS.check(!bl); + } + + @Override + public void checkHumanIdentificationIsPassed(List urls) { + for (String url : urls) { + checkHumanIdentificationIsPassed(url); + } + } +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/NsfwConfigServiceImpl.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/NsfwConfigServiceImpl.java new file mode 100644 index 0000000..14c897c --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/NsfwConfigServiceImpl.java @@ -0,0 +1,153 @@ +package com.sonic.shark.service.impl; + +import com.amazonaws.services.rekognition.model.DetectModerationLabelsResult; +import com.amazonaws.services.rekognition.model.ModerationLabel; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.shark.dao.NsfwConfigDao; +import com.sonic.shark.domain.bo.NsfwRecognitionResult; +import com.sonic.shark.domain.entity.NsfwConfig; +import com.sonic.shark.enums.S3BucketNameEnum; +import com.sonic.shark.service.NsfwConfigService; +import com.sonic.shark.utils.AwsRecognitionUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +@Slf4j +@Service +public class NsfwConfigServiceImpl extends ServiceImpl implements NsfwConfigService { + + @Autowired + private AwsRecognitionUtils awsRecognitionUtils; + + @Override + public NsfwRecognitionResult nsfwRecognition(S3BucketNameEnum s3BucketNameEnum, String fileFullPath) { + return nsfwRecognition(s3BucketNameEnum, fileFullPath, NsfwConfig.VersionType.DEFAULT.name()); + } + + + @Override + public NsfwRecognitionResult nsfwRecognition(S3BucketNameEnum s3BucketNameEnum, String fileFullPath, String versionType) { + //构造返回结果 + NsfwRecognitionResult result = new NsfwRecognitionResult(); + //获取图片识别的标签 + DetectModerationLabelsResult moderationLabelsResult = awsRecognitionUtils.nsfwRecognition(s3BucketNameEnum, fileFullPath); + //没有获取到标签,直接当审核通过处理 + if(moderationLabelsResult == null) { + result.setBl(false); + return result; + } + //获取标签列表 + List labelList = moderationLabelsResult.getModerationLabels(); + //获取使用的模型版本号 + String modelVersion = moderationLabelsResult.getModerationModelVersion(); + + result.setLabelList(labelList); + //根据版本号来判断看到底走那个逻辑(6.1版本走老逻辑,其他版本都走新逻辑) + //将标签评分和数据库配置的最小评分进行比较 得出鉴黄结果 + result.setBl(checkNsfw(labelList, versionType)); + return result; + } + + @Override + public Boolean checkNsfw(List labelList, String versionType) { + List categoryList = labelList.stream().map(e -> e.getName()).collect(Collectors.toList()); + if(CollectionUtils.isEmpty(categoryList)) { + return false; + } + Map categoryMap = getScore(categoryList, versionType); + if(categoryMap == null || categoryMap.isEmpty()) { + return false; + } + log.info("===> checkNsfw labelList all : {}", labelList); + //有三级分类,根据三级分类将所有的这个类型标签的数据过滤出来进行判断 + List level3LabelList = labelList.stream().filter(e -> e.getTaxonomyLevel() == 3).collect(Collectors.toList()); + if(CollectionUtils.isNotEmpty(level3LabelList)) { + //先做分数验证 + Boolean bl = checkScore(categoryMap, level3LabelList); + if(bl) { + return true; + } + //先过滤掉3级分类的数据 + labelList = labelList.stream().filter(e -> e.getTaxonomyLevel() != 3).collect(Collectors.toList()); + //根据3级分类找到2级分类 并过滤掉 + List level2LabelNameList = level3LabelList.stream().map(e -> e.getParentName()).collect(Collectors.toList()); + List level2LabelList = labelList.stream().filter(e -> level2LabelNameList.contains(e.getName())).collect(Collectors.toList()); + //过滤掉2级分类的数据 + labelList = labelList.stream().filter(e -> !level2LabelNameList.contains(e.getName())).collect(Collectors.toList()); + //根据2级分类找到1级分类 并过滤掉 + List level1LabelNameList = level2LabelList.stream().map(e -> e.getParentName()).collect(Collectors.toList()); + //过滤掉1级分类的数据 + labelList = labelList.stream().filter(e -> !level1LabelNameList.contains(e.getName())).collect(Collectors.toList()); + } + log.info("===> checkNsfw labelList level3 : {}", labelList); + //还有二级分类,根据二级分类将所有的这个类型标签的数据过滤出来进行判断 + List level2LabelList = labelList.stream().filter(e -> e.getTaxonomyLevel() == 2).collect(Collectors.toList()); + if(CollectionUtils.isNotEmpty(level2LabelList)) { + //先做分数验证 + Boolean bl = checkScore(categoryMap, level2LabelList); + if(bl) { + return true; + } + //先过滤掉2级分类的数据 + labelList = labelList.stream().filter(e -> e.getTaxonomyLevel() != 2).collect(Collectors.toList()); + //根据2级分类找到1级分类 并过滤掉 + List level1LabelNameList = level2LabelList.stream().map(e -> e.getParentName()).collect(Collectors.toList()); + //过滤掉1级分类的数据 + labelList = labelList.stream().filter(e -> !level1LabelNameList.contains(e.getName())).collect(Collectors.toList()); + } + log.info("===> checkNsfw labelList level2 : {}", labelList); + //还有一级分类,直接过滤出来进行判断 + List level1LabelList = labelList.stream().filter(e -> e.getTaxonomyLevel() == 1).collect(Collectors.toList()); + if(CollectionUtils.isNotEmpty(level1LabelList)) { + //先做分数验证 + Boolean bl = checkScore(categoryMap, level1LabelList); + if(bl) { + return true; + } + } + log.info("===> checkNsfw labelList level1 : {}", labelList); + return false; + } + + /** + * 标签得分校验操作 + * @param categoryMap + * @param list + * @return true 代表为黄图 + */ + private Boolean checkScore(Map categoryMap, List list) { + for (ModerationLabel label : list) { + Float score = categoryMap.get(label.getName()); + //字段解释:confidence 表示置信度,数值越高表示图片和标签说明越接近 + //aws没有返回标签分值 或者 数据库没有配置分值 则直接跳过当前标签的校验 + if(score == null || label.getConfidence() == null) { + continue; + } + //aws的值 大于 配置值 则 为黄图 + //数据库配置的分值越小越严格 数据库配置的分值越大越放宽 + if(label.getConfidence().compareTo(score) > 0) { + return true; + } + } + return false; + } + + @Override + public Map getScore(List categoryList, String versionType) { + List list = list(Wrappers.lambdaQuery() + .select(NsfwConfig::getCategory, NsfwConfig::getParentCategory, NsfwConfig::getScore) + .in(NsfwConfig::getCategory, categoryList) + .eq(StringUtils.isNotEmpty(versionType), NsfwConfig::getVersionType, versionType)); + return list.stream().collect(Collectors.toMap(NsfwConfig::getCategory, NsfwConfig::getScore)); + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/NsfwLogServiceImpl.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/NsfwLogServiceImpl.java new file mode 100644 index 0000000..5373df8 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/NsfwLogServiceImpl.java @@ -0,0 +1,40 @@ +package com.sonic.shark.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.sonic.common.utils.LogUtils; +import com.sonic.shark.dao.NsfwLogDao; +import com.sonic.shark.domain.bo.NsfwRecognitionResult; +import com.sonic.shark.domain.entity.NsfwLog; +import com.sonic.shark.enums.S3BucketNameEnum; +import com.sonic.shark.service.NsfwLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * 图鉴鉴黄日志表 + */ +@Slf4j +@Service +public class NsfwLogServiceImpl extends ServiceImpl implements NsfwLogService { + + + @Transactional(rollbackFor = Exception.class) + @Override + public void add(Long userId, S3BucketNameEnum s3BucketNameEnum, String fileFullPath, NsfwRecognitionResult nsfwRecognitionResult) { + NsfwLog log = new NsfwLog(); + log.setUserId(userId); + log.setBucketName(s3BucketNameEnum.getBucketName()); + log.setNsfwLabel(CollectionUtils.isEmpty(nsfwRecognitionResult.getLabelList()) ? null : JSONObject.toJSONString(nsfwRecognitionResult.getLabelList())); + log.setUrl(s3BucketNameEnum.getRootUrl() + fileFullPath); + log.setTraceId(LogUtils.getTraceId()); + log.setIsDelete(false); + log.setCreateTime(LocalDateTime.now()); + save(log); + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/OssServiceImpl.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/OssServiceImpl.java new file mode 100644 index 0000000..00c07be --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/OssServiceImpl.java @@ -0,0 +1,118 @@ +package com.sonic.shark.service.impl; + +import com.sonic.common.enums.AppEnv; +import com.sonic.shark.enums.ContentTypeEnum; +import com.sonic.shark.enums.S3BizTypeMappingEnum; +import com.sonic.shark.enums.S3BucketNameEnum; +import com.sonic.shark.service.OssService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CopyObjectRequest; +import software.amazon.awssdk.services.s3.model.CopyObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; + +import java.util.Random; + +/** + * @description: OSS文件上传 + * @author: code + **/ +@Slf4j +@Service +public class OssServiceImpl implements OssService { + @Autowired + private S3Client s3Client; + + @Value("${spring.profiles.active}") + private String runMode; + /** + * 测试环境的前缀目录 + */ + private static final String DEV_ENV_PREFIX = "dev/"; + + + @Override + public String uploadFileBytes(byte[] bytes, S3BizTypeMappingEnum s3BizTypeMappingEnum, String suffix) { + //环境参数 + String prefix = AppEnv.product.name().equals(runMode) ? s3BizTypeMappingEnum.getS3BucketNameEnum().getPrefix() : DEV_ENV_PREFIX; + //根据业务类型拼接出用户的相对路径 + String path = prefix + s3BizTypeMappingEnum.getFilePath(); + Random random = new Random(); + suffix = suffix == null ? "jpg" : suffix; + String fileName = System.currentTimeMillis() + "" + random.nextInt(10000) + "." + suffix; + S3BucketNameEnum s3BucketNameEnum = s3BizTypeMappingEnum.getS3BucketNameEnum(); + return uploadByBytes(bytes, s3BucketNameEnum, path, fileName, true); + } + + @Override + public String uploadByBytes(byte[] bytes, S3BucketNameEnum s3BucketNameEnum, String path, String fileName, boolean publicRead) { + try { + log.info("---------------- START UPLOAD FILE ----------------"); + log.info("uploadByBytes to bucket '" + s3BucketNameEnum.getBucketName()); + String uploadDir = path + fileName; + PutObjectRequest putObjectRequest; + if (publicRead) { + putObjectRequest = PutObjectRequest.builder() + .bucket(s3BucketNameEnum.getBucketName()) + .key(uploadDir) +// .acl(CannedAccessControlList.PublicRead.toString()) + .contentType(getContentType(fileName.substring(fileName.lastIndexOf(".")))) + .cacheControl("no-cache") + .build(); + } else { + putObjectRequest = PutObjectRequest.builder() + .bucket(s3BucketNameEnum.getBucketName()) + .key(uploadDir) +// .acl(CannedAccessControlList.Private.toString()) + .contentType(getContentType(fileName.substring(fileName.lastIndexOf(".")))) + .cacheControl("no-cache") + .build(); + } + PutObjectResponse putObjectResponse = s3Client.putObject( + putObjectRequest, + RequestBody.fromBytes(bytes)); + log.info("uploadByBytes putObjectResponse : {}", putObjectResponse); + log.info("===================== Upload File - Done! ====================="); + return s3BucketNameEnum.getRootUrl() + uploadDir; + } catch (Exception e) { + log.info("Exception e:" + e.toString()); + } + return null; + } + + @Override + public String copyFile(S3BucketNameEnum sourceS3BucketNameEnum, String sourceFilePath, S3BucketNameEnum targetS3BucketNameEnum, String targetFilePath) { + try { + Long startTime = System.currentTimeMillis(); + CopyObjectRequest copyRequest = CopyObjectRequest.builder() + .copySource(sourceS3BucketNameEnum.getBucketName() + "/" + sourceFilePath) + .destinationBucket(targetS3BucketNameEnum.getBucketName()) + .destinationKey(targetFilePath) + .build(); + CopyObjectResponse copyResponse = s3Client.copyObject(copyRequest); + log.info("===> copyFile status : {} ", copyResponse.copyObjectResult().eTag()); + log.info("===> time : {} ms", System.currentTimeMillis() - startTime); + } catch (Exception e) { + //复制失败,打印日志 + log.error("===> copyFile error : ", e); + } + return targetFilePath; + } + + /** + * 获取文件类型 + * + * @param fileSuffix + * @return + */ + public static String getContentType(String fileSuffix) { + String result = ContentTypeEnum.getContentType(fileSuffix); + return result == null ? "application/octet-stream" : result; + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/S3StsServiceImpl.java b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/S3StsServiceImpl.java new file mode 100644 index 0000000..8849de1 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/service/impl/S3StsServiceImpl.java @@ -0,0 +1,150 @@ +package com.sonic.shark.service.impl; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.securitytoken.AWSSecurityTokenService; +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; +import com.amazonaws.services.securitytoken.model.AssumeRoleRequest; +import com.amazonaws.services.securitytoken.model.AssumeRoleResult; +import com.sonic.common.enums.AppEnv; +import com.sonic.shark.domain.bo.S3StsToken; +import com.sonic.shark.enums.S3BizTypeMappingEnum; +import com.sonic.shark.enums.S3BucketNameEnum; +import com.sonic.shark.enums.ToastResultCode; +import com.sonic.shark.service.S3StsService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Random; + +@Slf4j +@Service +public class S3StsServiceImpl implements S3StsService { + + @Value("${aws.s3.sts.endpoint}") + private String endpoint; + @Value("${aws.s3.sts.accessKeyId}") + private String accessKeyId; + @Value("${aws.s3.sts.accessKeySecret}") + private String accessKeySecret; + @Value("${aws.s3.sts.roleArn}") + private String roleArn; + @Value("${aws.s3.sts.roleSessionName}") + private String roleSessionName; + @Value("${site.type:main}") + private String siteType; + + @Value("${spring.profiles.active}") + private String runMode; + + /** + * 测试环境的前缀目录 + */ + private static final String DEV_ENV_PREFIX = "dev/"; + + /** + * 根据业务类型获取用户上传文件的 STS token 文件目录授权 + * @param s3BizTypeMappingEnum + * @param userId + * @param suffix + * @return + */ + public S3StsToken getStsToken(S3BizTypeMappingEnum s3BizTypeMappingEnum, Long userId, String suffix) { + //环境参数 + String prefix = AppEnv.product.name().equals(runMode) ? s3BizTypeMappingEnum.getS3BucketNameEnum().getPrefix() : DEV_ENV_PREFIX; + //根据业务类型拼接出用户的相对路径 + String path = prefix + siteType + "/" + s3BizTypeMappingEnum.getFilePath() + userId; + //生成文件名 + Random random = new Random(); + String fileName; + if(S3BizTypeMappingEnum.SOUND_PATH == s3BizTypeMappingEnum) { + fileName = "*"; + } else { + fileName = System.currentTimeMillis() + "" + random.nextInt(10000) + "." + suffix; + } + S3StsToken s3StsToken = getStsTokenPolicy(s3BizTypeMappingEnum.getS3BucketNameEnum(), path, fileName); + s3StsToken.setFileName(fileName); + s3StsToken.setPath(s3StsToken.getPath() + fileName); + return s3StsToken; + } + + /** + * 获取STS token基础方法 到文件 + * @param bucketNameEnum + * @param path + * @param fileName + * @return + */ + public S3StsToken getStsTokenPolicy(S3BucketNameEnum bucketNameEnum, String path, String fileName) { + //判断哪些bucket是不能获取sts token的,直接抛出异常 + if(!bucketNameEnum.getS3StsTokenBl()) { + ToastResultCode.NO_OPERATION_AUTH.check(true); + } + String policy = "{\n" + + " \"Version\": \"2012-10-17\",\n" + + " \"Statement\": [{\n" + + " \"Effect\": \"Allow\",\n" + + " \"Action\": [\n" + + " \"s3:PutObject\",\n" + + " \"s3:PutObjectAcl\",\n" + + " \"s3:GetObject\",\n" + + " \"s3:PutObjectTagging\"\n" + + " ],\n" + + " \"Resource\": [\n" + + " \"arn:aws:s3:::" + bucketNameEnum.getBucketName() + "/" + path + "/" + fileName + "\"\n" + + " ]\n" + + " }]\n" + + "}"; + log.info("===> getStsToken policy : {}", policy); + return getStsToken(bucketNameEnum.getRegion().toString(), path, policy); + } + + /** + * 获取S3的STS授权token 底层方法 + * + * @param policy 授权策略 + * @return + */ + public S3StsToken getStsToken(String region, String path, String policy) { + try { + // Creating the STS client is part of your trusted code. It has + // the security credentials you use to obtain temporary security credentials. + AWSCredentials credentials = new BasicAWSCredentials(accessKeyId, accessKeySecret); + + AWSSecurityTokenService stsClient = AWSSecurityTokenServiceClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + + // Obtain credentials for the IAM role. Note that you cannot assume the role of an AWS root account; + // Amazon S3 will deny access. You must use credentials for an IAM user or an IAM role. + // 默认情况下,会话的持续时间为一个小时。如果您使用了 IAM 用户凭证,则可在请求临时安全凭证时指定持续时间(15 分钟到角色的最长会话持续时间) + //可配置范围:15 分钟 – 12 小时 + // https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/AuthUsingTempSessionToken.html + com.amazonaws.services.securitytoken.model.AssumeRoleRequest roleRequest = new AssumeRoleRequest() + .withRoleArn(roleArn) + .withRoleSessionName(roleSessionName) + .withPolicy(policy) + .withDurationSeconds(60 * 60); + AssumeRoleResult response = stsClient.assumeRole(roleRequest); + + S3StsToken s3StsToken = S3StsToken.builder() + .expiration(response.getCredentials().getExpiration().toString()) + .accessKeyId(response.getCredentials().getAccessKeyId()) + .accessKeySecret(response.getCredentials().getSecretAccessKey()) + .securityToken(response.getCredentials().getSessionToken()) + .path(path + "/") + .build(); + + return s3StsToken; + } catch (Exception e) { + // The call was transmitted successfully, but Amazon S3 couldn't process + // it, so it returned an error response. + e.printStackTrace(); + } + return null; + } + +} diff --git a/sonic-shark/server/src/main/java/com/sonic/shark/utils/AwsRecognitionUtils.java b/sonic-shark/server/src/main/java/com/sonic/shark/utils/AwsRecognitionUtils.java new file mode 100644 index 0000000..da029b0 --- /dev/null +++ b/sonic-shark/server/src/main/java/com/sonic/shark/utils/AwsRecognitionUtils.java @@ -0,0 +1,192 @@ +package com.sonic.shark.utils; + +import com.alibaba.fastjson.JSONObject; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.services.rekognition.AmazonRekognition; +import com.amazonaws.services.rekognition.AmazonRekognitionClientBuilder; +import com.amazonaws.services.rekognition.model.*; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.sonic.shark.domain.bo.FaceFeaturesBo; +import com.sonic.shark.enums.S3BucketNameEnum; +import com.sonic.shark.enums.ToastResultCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 亚马逊 图片 识别工具类 + * @Author code + */ +@Slf4j +@Component +public class AwsRecognitionUtils { + + @Value("${aws.s3.recognition.accessKeyId}") + private String accessKey; + @Value("${aws.s3.recognition.secretAccessKey}") + private String secretAccessKey; + + private Map clientMap = new ConcurrentHashMap<>(); + + private AWSCredentials credentials; + + @PostConstruct + public void init() { + credentials = new YmlCredentials(accessKey, secretAccessKey); + } + + /** + * 获取client对象 + * @param s3BucketNameEnum + * @return + */ + public AmazonRekognition getClient(S3BucketNameEnum s3BucketNameEnum) { + AmazonRekognition client = clientMap.get(s3BucketNameEnum); + if(client == null) { + client = AmazonRekognitionClientBuilder.standard() + .withRegion(s3BucketNameEnum.getRegions()) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + clientMap.put(s3BucketNameEnum, client); + } + return client; + } + + /** + * S3 黄色暴力 鉴定 + * https://docs.aws.amazon.com/rekognition/latest/dg/moderation.html + * @param s3BucketNameEnum + * @param fileFullPath + * @return + */ + public DetectModerationLabelsResult nsfwRecognition(S3BucketNameEnum s3BucketNameEnum, String fileFullPath) { + try { + Long startTime = System.currentTimeMillis(); + DetectModerationLabelsRequest request = new DetectModerationLabelsRequest() + .withImage(new Image().withS3Object(new S3Object().withName(fileFullPath).withBucket(s3BucketNameEnum.getBucketName()))) + .withMinConfidence(60F); + //先不主动指定使用模型的版本号,让aws来主动决定进行自动切换 +// .withRequestedModelVersion(ModerationRequestedModelVersion.V7_0); + log.info("===> nsfwRecognition request : {}", request.toString()); + DetectModerationLabelsResult result = getClient(s3BucketNameEnum).detectModerationLabels(request); + log.info("===> nsfwRecognition time : {} ms, modelVersion : {}, labels : {}", System.currentTimeMillis() - startTime, result.getModerationModelVersion(), result.getModerationLabels()); + return result; + } catch (InvalidImageFormatException e) { + log.error("===> AwsRecognitionUtils nsfwRecognition error: ", e); + boolean isGif = fileFullPath.endsWith(".gif"); + //图片格式无效的异常提示 + ToastResultCode.SYSTEM_EXCEPTION.check(!isGif, "Invalid image format"); + return null; + } catch (Exception e) { + log.error("===> AwsRecognitionUtils nsfwRecognition error: ", e); + ToastResultCode.SYSTEM_EXCEPTION.check(true); + return null; + } + } + + + /** + * 图片标签识别 + * @param s3BucketEnum + * @param fileFullPath + * @return + */ + public String imageTagRecognition(S3BucketNameEnum s3BucketEnum, String fileFullPath) { + try { + DetectLabelsRequest request = new DetectLabelsRequest() + .withImage(new Image().withS3Object(new S3Object().withName(fileFullPath).withBucket(s3BucketEnum.getBucketName()))) + .withMaxLabels(150).withMinConfidence(30F); + DetectLabelsResult result = getClient(s3BucketEnum).detectLabels(request); + List