暂时未有相关云产品技术能力~
背景 之前介绍过springboot集成webstock方式,具体参考:springboot集成websocket实战:站内消息实时推送这里补充另外一个使用webstock的场景,方便其他同学理解和使用,废话不多说了,直接开始!简单介绍一下业务场景: 现在有一个投票活动,活动详情中会显示投票活动的参与人数、访问数、投票数。这三个投票数据需要实时的进行变化,这里就可以使用webstock进行返回页面。当三个数据发生变化时,服务端发送最新数据给客户端,客户端仅进行展示即可,不用轮询查询数据,页面也会显示动态效果。实现步骤1.配置webstock@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }2.webstocket服务逻辑说明 每个客户端进入活动之后需要与服务端建立一个连接,由于是在微信小程序中进行发布活动,所以使用用户的openID进行用户的唯一标识,每个链接就可以看做是客户端与服务端的一次会话session,使用clients将所有登录的用户创建的session存储起来,方便后期进行消息群发操作,用户退出时将创建session进行关闭操作。投票数、访问量、参与人数,每个指标变化时需要调用webstock服务器中的群发消息功能,这样就能保证三个数据变化之后客户端可以实时显示出最新的数据,达到最终预期的效果。3.webstocket服务代码实现@Component @Slf4j @Service @ServerEndpoint("/ws/{openId}") public class WebSocketServer { // 每个在线用户会创建一个WebSocketServer对象 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>(); // 存放所有在线的客户端 key为用户的唯一标识:userId,value为每个会话连接 private static Map<String, Session> clients = new ConcurrentHashMap<>(); /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("openId") String openId) throws IOException { log.info("dataId:{},已建立连接",openId); clients.put(openId, session); webSocketSet.add(this); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(Session session,@PathParam("openId") String openId) { log.info("dataId:{},关闭连接",openId); clients.remove(openId); webSocketSet.remove(this); //从set中删除 } /** * @ Param session * @ Param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); } /** * @Author: txm * @Description: 群发消息 * @Param: [] * @return: void * @Date: 2022/12/23 14:28 **/ public void batchSendMsg() throws IOException { VoteServiceImpl voteService = SpringUtils.getBean(VoteServiceImpl.class); // 此处是组装返回类似的参数信息:{"joinCount":15,"visitCount":169,"voteCount":36} String voteStatisticsVo = voteService.findVoteStatisticsVo(); for (Map.Entry<String, Session> integerSessionEntry : clients.entrySet()) { integerSessionEntry.getValue().getBasicRemote().sendText(voteStatisticsVo); } } }4.业务逻辑触发说明 这里仅以增加访问量操作为例进行说明(投票数增加、参与人数增加同理,只是业务触发逻辑不同),自定义了一个增加访问量的接口,前端调用该接口则活动的访问数量会进行加一处理,webstock服务群发消息的处理就需要在这里完成,参考代码如下:@Service @Slf4j public class VoteServiceImpl implements VoteService { @Autowired private WebSocketServer webSocketServer; public synchronized void updateVisitCount(Integer voteId) throws IOException { System.out.println("投票活动访问量增加操作"); // 服务端群发消息,更新统计数据 webSocketServer.batchSendMsg(); } }5.客户端测试地址:http://1json.com/network/ws.html,输入项目地址. 调用更新访问量接口之后,可以看到服务端已经将最新的投票统计信息进行了返回处理:问题说明 关于配置完成之后测试客户端不能建立连接问题 当时在测试的时候也出现过不能建立连接,分析了一下原因是项目中使用了ssl证书配置,也就是项目本身访问的方式是https访问,所以不能使用ws协议进行访问,需要使用wss协议进行访问(两者的区别可以类比理解为http与https的区别),简单说一下项目配置,没有为了适配wss单独做配置(支持https访问的项目也会支持wss访问).配置文件:server: port: 8083 servlet: session: timeout: PT30M ssl: enabled: true key-store: classpath:config/ssl/*****.com.pfx key-store-password: ***** keyStoreType: *****证书路径:说一下本地以及测试环境的访问方式:本地访问:wss://127.0.0.1:8083/ws/1服务器访问(使用ip访问不通,只能使用域名):wss://域名:8083/ws/1以上是使用webstock实现页面数据实时刷新的实现过程,如果看完感觉有所帮助欢迎评论区留言或点赞!
今天在接手的项目中本想在测试类中跑一遍持久层的逻辑,但是测试类型项目启动就报错,报错信息如下:Internal Error occurred. org.junit.platform.commons.JUnitException: TestEngine with ID 'junit-jupiter' failed to discover tests at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverEngineRoot(EngineDiscoveryOrchestrator.java:111) at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:85) at org.junit.platform.launcher.core.DefaultLauncher.discover(DefaultLauncher.java:92) at org.junit.platform.launcher.core.DefaultLauncher.discover(DefaultLauncher.java:67) at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:48) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) Caused by: org.junit.platform.commons.JUnitException: MethodSelector [className = 'com.kawa.job.manage.JobManageTest', methodName = 'test', methodParameterTypes = ''] resolution failed at org.junit.platform.launcher.listeners.discovery.AbortOnFailureLauncherDiscoveryListener.selectorProcessed(AbortOnFailureLauncherDiscoveryListener.java:39) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolveCompletely(EngineDiscoveryRequestResolution.java:102) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.run(EngineDiscoveryRequestResolution.java:82) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver.resolve(EngineDiscoveryRequestResolver.java:113) at org.junit.jupiter.engine.discovery.DiscoverySelectorResolver.resolveSelectors(DiscoverySelectorResolver.java:45) at org.junit.jupiter.engine.JupiterTestEngine.discover(JupiterTestEngine.java:69) at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverEngineRoot(EngineDiscoveryOrchestrator.java:103) ... 7 more Caused by: java.lang.UnsupportedClassVersionError: org/springframework/test/context/BootstrapWith has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 52.0 at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) at java.net.URLClassLoader.access$100(URLClassLoader.java:73) at java.net.URLClassLoader$1.run(URLClassLoader.java:368) at java.net.URLClassLoader$1.run(URLClassLoader.java:362) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:361) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) at sun.reflect.generics.factory.CoreReflectionFactory.makeNamedType(CoreReflectionFactory.java:114) at sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:125) at sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:49) at sun.reflect.annotation.AnnotationParser.parseSig(AnnotationParser.java:439) at sun.reflect.annotation.AnnotationParser.parseAnnotation2(AnnotationParser.java:241) at sun.reflect.annotation.AnnotationParser.parseAnnotations2(AnnotationParser.java:120) at sun.reflect.annotation.AnnotationParser.parseSelectAnnotations(AnnotationParser.java:101) at sun.reflect.annotation.AnnotationType.<init>(AnnotationType.java:145) at sun.reflect.annotation.AnnotationType.getInstance(AnnotationType.java:85) at sun.reflect.annotation.AnnotationParser.parseAnnotation2(AnnotationParser.java:266) at sun.reflect.annotation.AnnotationParser.parseAnnotations2(AnnotationParser.java:120) at sun.reflect.annotation.AnnotationParser.parseAnnotations(AnnotationParser.java:72) at java.lang.Class.createAnnotationData(Class.java:3521) at java.lang.Class.annotationData(Class.java:3510) at java.lang.Class.getDeclaredAnnotation(Class.java:3458) at org.junit.platform.commons.util.AnnotationUtils.findAnnotation(AnnotationUtils.java:128) at org.junit.platform.commons.util.AnnotationUtils.findAnnotation(AnnotationUtils.java:115) at org.junit.jupiter.engine.descriptor.DisplayNameUtils.determineDisplayName(DisplayNameUtils.java:68) at org.junit.jupiter.engine.descriptor.JupiterTestDescriptor.<init>(JupiterTestDescriptor.java:69) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.<init>(ClassBasedTestDescriptor.java:94) at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.<init>(ClassTestDescriptor.java:51) at org.junit.jupiter.engine.discovery.ClassSelectorResolver.newClassTestDescriptor(ClassSelectorResolver.java:119) at org.junit.jupiter.engine.discovery.ClassSelectorResolver.lambda$resolve$0(ClassSelectorResolver.java:71) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution$DefaultContext.createAndAdd(EngineDiscoveryRequestResolution.java:246) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution$DefaultContext.addToParent(EngineDiscoveryRequestResolution.java:209) at org.junit.jupiter.engine.discovery.ClassSelectorResolver.resolve(ClassSelectorResolver.java:71) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.lambda$resolve$2(EngineDiscoveryRequestResolution.java:134) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1359) at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126) at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:464) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolve(EngineDiscoveryRequestResolution.java:185) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolve(EngineDiscoveryRequestResolution.java:125) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.access$100(EngineDiscoveryRequestResolution.java:57) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution$DefaultContext.resolve(EngineDiscoveryRequestResolution.java:224) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution$DefaultContext.addToParent(EngineDiscoveryRequestResolution.java:218) at org.junit.jupiter.engine.discovery.MethodSelectorResolver$MethodType.resolve(MethodSelectorResolver.java:187) at org.junit.jupiter.engine.discovery.MethodSelectorResolver$MethodType.access$300(MethodSelectorResolver.java:143) at org.junit.jupiter.engine.discovery.MethodSelectorResolver.lambda$resolve$0(MethodSelectorResolver.java:89) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) at org.junit.jupiter.engine.discovery.MethodSelectorResolver.resolve(MethodSelectorResolver.java:93) at org.junit.jupiter.engine.discovery.MethodSelectorResolver.resolve(MethodSelectorResolver.java:73) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.lambda$resolve$2(EngineDiscoveryRequestResolution.java:146) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1359) at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126) at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:464) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolve(EngineDiscoveryRequestResolution.java:185) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolve(EngineDiscoveryRequestResolution.java:125) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolveCompletely(EngineDiscoveryRequestResolution.java:91) ... 12 more 仔细检查之后发现pom.xml中不仅添加了spring-boot-starter-test依赖,还添加了spring-test依赖,将spring-test注释掉之后项目启动成功,猜测原因可能是项目启动先执行的是spring-test依赖解析,看过spring-test的实现就知道没有里面没有junit-jupiter.spring-test内容如下:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <!-- This module was also published with a richer model, Gradle metadata, --> <!-- which should be used instead. Do not delete the following line which --> <!-- is to indicate to Gradle or any Gradle module metadata file consumer --> <!-- that they should prefer consuming it instead. --> <!-- do_not_remove: published-with-gradle-metadata --> <modelVersion>4.0.0</modelVersion> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>6.0.3</version> <name>Spring TestContext Framework</name> <description>Spring TestContext Framework</description> <url>https://github.com/spring-projects/spring-framework</url> <organization> <name>Spring IO</name> <url>https://spring.io/projects/spring-framework</url> </organization> <licenses> <license> <name>Apache License, Version 2.0</name> <url>https://www.apache.org/licenses/LICENSE-2.0</url> <distribution>repo</distribution> </license> </licenses> <developers> <developer> <id>jhoeller</id> <name>Juergen Hoeller</name> <email>jhoeller@pivotal.io</email> </developer> </developers> <scm> <connection>scm:git:git://github.com/spring-projects/spring-framework</connection> <developerConnection>scm:git:git://github.com/spring-projects/spring-framework</developerConnection> <url>https://github.com/spring-projects/spring-framework</url> </scm> <issueManagement> <system>GitHub</system> <url>https://github.com/spring-projects/spring-framework/issues</url> </issueManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>6.0.3</version> <scope>compile</scope> </dependency> </dependencies> </project>
刚入门uniapp,简单记录一下希望对有同样问题的小伙伴有所帮助,尽量少浪费时间. 新创建的uniapp项目,项目启动之后就报错,是不是很挫败,第一步就有问题!!!具体报错信息如下:Uncaught TypeError: Cannot read property 'meta' of undefined控制台截图: 发现将pages.json中的tabbar节点全部注释掉之后启动正常,进一步来看将tabbar中的list属性注释掉之后项目启动也是正常的.到这里就是页面路径的问题.仔细观察发现项目pages下面的页面路径信息没有配置,导致tabbar中加载页面异常,按照页面加载顺序补充完整即可.项目启动是按照pages下面的页面的顺序进行加载,有文件但是配置中没有加载就会报错,tabbar的list属性中再去找对应的页面更会报错.正常的pages页面配置如下:pages对象:"pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "uni-app" } }, { "path": "pages/message/message", "style": { "navigationBarTitleText": "", "enablePullDownRefresh": false } } ]页面截图:tabbar页面配置:"tabBar": { //底部 "color": "#999999", "selectedColor": "#09C160", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [{ "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/message/message", "text": "消息" } ] }页面截图如下:
1.背景说明 一般一个项目中只会连接一个数据库.但是随着需求变更,会要求同一个项目中连接多个数据库,本文就讲一下如何在一个项目中对多个数据库进行连接.本文基于springboot+mybatis介绍如何进行多数据源连接(本文演示配置两个数据库,配置多个同理).2.配置多数据源步骤2.1 项目结构变更 以连接不同的数据库进行分包,db1表示连接数据库database1,db2表示连接database2.项目结构如下:2.2添加配置类/** * @ClassName: MultiDataSourceConfig * @Desc: 多数据源配置 * @Author: txm * @Date: 2022/12/11 9:59 **/ @Configuration public class MultiDataSourceConfig { // 创建自定义数据源db1 @Bean(name = "db1") @ConfigurationProperties(prefix = "spring.datasource.db1") public DataSource businessDbDataSource() { return DataSourceBuilder.create().build(); } // 创建自定义数据源db2 @Bean(name = "db2") @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource newhomeDbDataSource() { return DataSourceBuilder.create().build(); } }Db1Config配置类:@Configuration // 此处添加项目自定义mapper包路径,支持数组形式,指定SqlSessionFactory ,演示包路径已做修改 @MapperScan(basePackages = {"com.api.db1.order.mapper", "com.api.db1.pay.mapper", "com.api.db1.user.mapper", "com.api.db1.distribution.mapper", "com.api.db1.aliyun.mapper" }, sqlSessionFactoryRef = "sqlSessionFactoryDb1") public class Db1Config { @Autowired @Qualifier("db1") private DataSource dataSourceDb1; @Bean public SqlSessionFactory sqlSessionFactoryDb1() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSourceDb1); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/db1/**/*.xml")); // 自定义MyBatis configuration.配置打印和大小写转换 org.apache.ibatis.session.Configuration personalConfiguration = new org.apache.ibatis.session.Configuration(); personalConfiguration.setMapUnderscoreToCamelCase(true); personalConfiguration.setLogImpl(StdOutImpl.class); factoryBean.setConfiguration(personalConfiguration); return factoryBean.getObject(); } @Bean public SqlSessionTemplate sqlSessionTemplateDb1() throws Exception { return new SqlSessionTemplate(sqlSessionFactoryDb1()); } }Db2Config配置类:@Configuration // 此处添加项目自定义mapper包路径,支持数组形式,指定SqlSessionFactory ,演示包路径已做修改 @MapperScan(basePackages = {"com.api.db2.activityUser.mapper", "com.api.db2.vote.mapper"}, sqlSessionFactoryRef = "sqlSessionFactoryDb2") public class Db2Config { @Autowired @Qualifier("db2") private DataSource dataSourceDb2; @Bean public SqlSessionFactory sqlSessionFactoryDb2() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSourceDb2); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/db2/**/*.xml")); // 自定义MyBatis configuration.配置打印和大小写转换 org.apache.ibatis.session.Configuration personalConfiguration = new org.apache.ibatis.session.Configuration(); personalConfiguration.setMapUnderscoreToCamelCase(true); personalConfiguration.setLogImpl(StdOutImpl.class); factoryBean.setConfiguration(personalConfiguration); return factoryBean.getObject(); } @Bean public SqlSessionTemplate sqlSessionTemplateDb2() throws Exception { return new SqlSessionTemplate(sqlSessionFactoryDb2()); } } 说明 自定义配置类Db1Config 、Db2Config中指定 @MapperScan之后可以将启动类上面的@MapperScan注解进行注释,@MapperScan的作用就是扫描mapper所在的包路径:// modify by txm 2022/12/12 多数据源改造,包扫描添加到自定义数据源配置类Db1Config、Db2Config中 //@MapperScan("com.**.mapper") @SpringBootApplication public class H5Application { public static void main(String[] args) { SpringApplication.run(H5Application.class, args); } } 配置类中指定mapper配置文件所在路径,否则会提示:org.apache.ibatis.binding.BindingException: Invalid bound statement (not found),添加内容如下::factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/db2/**/*.xml")); 对应的配置文件是(可以进行注释):mybatis: # 配置数据库映射文件 **表示是任意多级目录,适配dao/goods mapper-locations: classpath:/mapper/**/**/*.xml 配置mybatis的日志打印与大小写转换:// 自定义MyBatis configuration.配置打印和大小写转换 org.apache.ibatis.session.Configuration personalConfiguration = new org.apache.ibatis.session.Configuration(); personalConfiguration.setMapUnderscoreToCamelCase(true); personalConfiguration.setLogImpl(StdOutImpl.class); factoryBean.setConfiguration(personalConfiguration); 添加之后配置文件中可以将以下内容进行注释:mybatis: # 配置数据库映射文件 **表示是任意多级目录,适配dao/goods mapper-locations: classpath:/mapper/**/**/*.xml #spring boot集成mybatis的方式打印sql configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #spring boot记成mybatis将数据库字段字段转换华成驼峰形式 map-underscore-to-camel-case: true2.3 修改配置文件数据连接配置信息spring:#数据库信息设置 datasource: db1: # database1服务器地址 jdbc-url: jdbc:mysql://xxx.xxx.xx.xx:3308/database1?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true username: **** password: **** driver-class-name: com.mysql.cj.jdbc.Driver db2: # database2服务器地址 jdbc-url: jdbc:mysql://xxx.xxx.xx.xx:3308/database2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true username: **** password: **** driver-class-name: com.mysql.cj.jdbc.Driver # 配置数据库连接池 type: com.zaxxer.hikari.HikariDataSource hikari: minimum-idle: 5 # 最小空闲连接数量 idle-timeout: 180000 # 单位毫秒,此处设置3分钟,空闲连接存活最大时间,默认600000(10分钟) maximum-pool-size: 10 # 连接池最大连接数,默认是10 auto-commit: true # 此属性控制从池返回的连接的默认自动提交行为,默认值:true pool-name: MyHikariCP # 连接池名称 max-lifetime: 1800000 # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟 connection-timeout: 30000 # 数据库连接超时时间,默认30秒,即30000 注意:要将url替换成jdbc-url,否则会提示以下内容:java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName. 关于数据库连接类型可以选用springboot默认支持的Hikari,性能会更好!如果对Hikari无配置要求,可以不用添加Hikari相关的配置信息.演示内容中配置了db2的Hikari信息. 以上是关于springboot中配置多数据源的实战记录,希望对有同样需求的同学有所帮助,感觉有所收获欢迎点赞或是留言!
1.属性绑定:父组件传递参数到子组件 首先交代一下基本的项目信息:主页面为index.wxml,创建test子组件,文件目录:component/test/test,index.json中引用test组件.{ "component": true, "usingComponents": { "test":"/component/test/test" } }演示案例: 父组件中定义num并初始化数据为1,将num传递到子组件test中并展示.index.js中定义num并初始化数据为1Page({ data: { num:1 } })index.html中创建view组件<view>父组件属性:{{num}}</view>test.html中接收父组件中数据:<view>子组件获取到父组件的属性:{{num}}</view>test.js中定义num属性:Componet({ properties:{ num:Number } })页面展示效果如下:2.事件绑定:子组件传递参数到父组件 操作步骤:1.父组件js中定义函数;2.父组件中通过传递自定义事件形式传递给子组件;3.子组件js中调用父组件自定义事件;this.triggerEvent('父组件函数', {value: 子组件参数});3.父组件自定义事件处理子组件传递参数,通过event.detail.value获取 示例说明:index.wxml中添加test组件,test组件创建初始值subCount和加一按钮,点击按钮实现subCount加一.父组件中创建初始值parCount,实现test子组件中点击按钮之后不仅能实现subCout加一,也能将subCount赋值给parCount.即点击按钮实现subCount与parCount同步变化.子组件test中初始化subCount以及创建按钮实现点击加一操作:test.wxml:<view>子组件data属性subCount值:{{subCount}}</view> <button type="primary" bindtap="subCountAdd">子组件中点击数据值加一</button>test.js中:Component({ data:{ subCount:1 }, methods:{ subCountAdd:function(){ this.setData({ subCount: this.data.subCount+1 }) }} })父组件index.js中初始化parCount并创建传递参数函数,通过参数event.detail.value来获取传递参数:Page({ data: { parCount:2 }, // 自定义父组件函数 transferFucntion(event){ console.log("父组件中传参事件触发!"), console.log(event.detail) } } )父组件index.wxml中在子组件test中传递自定义函数:<test bind:transferFucntion="transferFucntion"></test> <view>父组件data属性parCount值:{{parCount}}</view>子组件test.js中的加一方法中添加父组件的自定义事件,传递参数为Component({ data:{ subCount:1 }, methods:{ subCountAdd:function(){ this.setData({ subCount: this.data.subCount+1 }), // 注册父组件方法,this.triggerEvent('父组件函数名',参数(非必填)) this.triggerEvent('transferFucntion', {value: this.data.subCount}); }} })父组件index.js中通过参数event.detail.value来获取传递参数并赋值给parCount:Page({ data: { parCount:2 }, // 自定义父组件函数 transferFucntion(event){ console.log("父组件中传参事件触发!"), console.log(event.detail), this.setData({ parCount:event.detail.value }) } } )最终实现效果,点击按钮,子组件中subCount与parCount同步变化.4.获取组件对象实例:父组件获取子组件实例对象进行参数传递 父组件中使用this.selectComponent('子组件id选择器或是class选择器)进行获取子组件实例,进而传递参数到子组件.演示示例: 父组件index.wxml中创建点击事件获取子组件test中subCount值以及调用子组件的subCountAdd方法(subCount与subCountAdd均在上个案例中定义过) index.wxml中创建点击事件selectSubCompnet并给子组件test设置id属性和class属性:<test id="subCompnetId" class="subCompnetClass"></test> <button type="primary" bindtap="selectSubCompnet">点击获取子组件</button>index.js中点击事件获取子组件实例并获取子组件属性和方法并调用:Page({ // 父组件选择子组件 selectSubCompnet(){ const subCompnet=this.selectComponent('#subCompnetId'); console.log("subCount值:"+subCompnet.data.subCount); subCompnet.subCountAdd(); console.log("调用子组件subCountAdd方法之后subCount值:"+subCompnet.data.subCount); } })点击按钮并观察控制台输出内容:观察可以发现子组件subCount与subCountAdd方法均被调用;使用this.setDate即可更改子组件属性.
1.背景说明 近期营销活动中需要商户转账到微信用户零钱,实战角度说下接入过程,期间用的时间也比较多,把遇到的问题以及如何处理问题过程记录一下,希望对有同样需求的同学有所帮助,尽量少用一些时间,更专注业务处理.本文仅以发起商家转账( /v3/transfer/batches)功能进行讲解.2.实现过程2.1接入之前的准备工作 开通微信商户账号以及开通商家转账到零钱产品功能并对指定功能进行相关设置.官方接入的详情地址:https://pay.weixin.qq.com/docs/merchant/products/batch-transfer-to-balance/preparation.html2.2 代码实现controller: @ApiOperation("提现到零钱") @PostMapping("/transferAccount") public ResultVo transferAccount(String openId) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, KeyStoreException { payService.transferAccount(openId); return ResultVoUtil.success(); }service:public interface PayService { // add by txm 2022/10/29 提现到微信零钱 void transferAccount(String openIdId) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, IOException, KeyStoreException; }参数实体类:@ApiModel("转账请求参数") @Data public class TransferDto { @ApiModelProperty(value = "直连商户的appid",example = "123",dataType = "String") private String appid; @ApiModelProperty(value = "商家批次单号",example = "plfk2020042013",dataType = "String") private String out_batch_no; @ApiModelProperty(value = "批次名称",example = "2019年1月深圳分部报销单",dataType = "String") private String batch_name; @ApiModelProperty(value = "批次备注",example = "2019年1月深圳分部报销单",dataType = "String") private String batch_remark; @ApiModelProperty(value = "转账总金额,单位分",example = "1",dataType = "Integer") private Integer total_amount; @ApiModelProperty(value = "转账总笔数",example = "1",dataType = "Integer") private Integer total_num; @ApiModelProperty(value = "转账明细列表",dataType = "list.class") private List<TransferDetailDto> transfer_detail_list=new ArrayList<>(); }@ApiModel("转账请求详情参数") @Data public class TransferDetailDto { @ApiModelProperty(value = "商家明细单号(相当于子订单)",example = "x23zy545Bd5436",dataType = "String") private String out_detail_no; @ApiModelProperty(value = "转账金额,单位分",example = "2",dataType = "Integer") private Integer transfer_amount; @ApiModelProperty(value = "转账备注",example = "2020年4月报销",dataType = "String") private String transfer_remark; @ApiModelProperty(value = "用户在直连商户应用下的用户标示",example = "2019年1月深圳分部报销单",dataType = "String") private String openid; }业务实现类:@Slf4j @Service public class PayServiceImpl implements PayService { /** * @Author: txm * @Description: 转账逻辑 * @Param: [method, url, body] * @return: java.lang.String * @Date: 2022/11/24 16:08 **/ @Override public void transferAccount(String openIdId) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, IOException, KeyStoreException { // 组装转账到零钱参数 TransferDto transferDto = new TransferDto(); transferDto.setAppid("小程序APPID或是公众号id"); String out_batch_no = RandomUtil.randomNumbers(10); out_batch_no=StrUtil.concat(true,"sc",out_batch_no); transferDto.setOut_batch_no(out_batch_no); transferDto.setBatch_name("test1"); transferDto.setBatch_remark("test2"); transferDto.setTotal_amount(1); transferDto.setTotal_num(1); TransferDetailDto transferDetailDto = new TransferDetailDto(); String out_detail_no = RandomUtil.randomNumbers(10); out_detail_no=StrUtil.concat(true,"detail",out_detail_no); transferDetailDto.setOut_detail_no(out_detail_no); transferDetailDto.setTransfer_amount(1); transferDetailDto.setTransfer_remark("test3"); transferDetailDto.setOpenid(openIdId); transferDto.getTransfer_detail_list().add(transferDetailDto); String transferDtoStr = JSONUtil.toJsonStr(transferDto); // 组装Authorization信息 HttpUrl httpUrl = HttpUrl.get("https://api.mch.weixin.qq.com/v3/transfer/batches"); String tokenInfo=getToken("POST",httpUrl,transferDtoStr); log.info("Authorization认证信息:{}",tokenInfo); // Authorization认证类型 String authType="WECHATPAY2-SHA256-RSA2048"; // Authorization信息 认证类型 认证信息,此处使用hutool工具类中concat进行拼接,注意两部分中间用空格分割 String authorization= StrUtil.concat(true, authType," ",tokenInfo); // 发送请求 String returnMsg = HttpRequest.post("https://api.mch.weixin.qq.com/v3/transfer/batches") .header("Authorization", authorization) .header("Wechatpay-Serial","证书序列号") .body(transferDtoStr) .execute().body(); JSONObject returnTransferInfo = JSON.parseObject(returnMsg); log.info("转账申请返回信息:{}",returnTransferInfo); } /** * @Author: txm * @Description: 获取Authorization认证签名信息 * @Param: [method, url, body] * @return: java.lang.String * @Date: 2022/11/24 16:08 **/ public String getToken(String method, HttpUrl url, String body) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, KeyStoreException { // 随机字符串 String nonceStr = RandomUtil.randomString(26); // 时间戳,单位秒 long timestamp = System.currentTimeMillis() / 1000; // 组装签名串信息 String message = buildMessage(method, url, timestamp, nonceStr, body); log.info("签名串:{}",message); // 签名串加密处理 String signature = sign(message.getBytes("utf-8")); log.info("签名信息:{}",signature); return "mchid=\"" + "商户id" + "\"," + "serial_no=\"" + "证书序列号" + "\"," + "nonce_str=\"" + nonceStr + "\"," + "timestamp=\"" + timestamp + "\"," + "signature=\"" + signature + "\""; } /** * @Author: txm * @Description: 组装签名请求信息 * @Param: [method, url, timestamp, nonceStr, body] * @return: java.lang.String * @Date: 2022/11/24 16:09 **/ public String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) { String canonicalUrl = url.encodedPath(); if (url.encodedQuery() != null) { canonicalUrl += "?" + url.encodedQuery(); } return method + "\n" + canonicalUrl + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n"; } /** * @Author: txm * @Description: 签名加密 * @Param: [byte[]] * @return: java.lang.String * @Date: 2022/11/24 16:09 **/ public String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, KeyStoreException, IOException { Signature sign = Signature.getInstance("SHA256withRSA"); // apiclient_key.pem存在到resource/config/cert下 Resource resource = resourceLoader.getResource("classpath:/config/cert/apiclient_key.pem"); File file = resource.getFile(); String path = file.getPath(); // 获取私钥key,实际读取apiclient_key.pem文件信息创建PrivateKey 对象 PrivateKey privateKey = getPrivateKey(path); sign.initSign(privateKey); sign.update(message); return Base64.encodeBase64String(sign.sign()); } /** * @Author: txm * @Description: 获取PrivateKey * @Param: [byte[]] * @return: java.lang.String * @Date: 2022/11/24 16:09 **/ public static PrivateKey getPrivateKey(String filename) throws IOException { String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8"); try { String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePrivate( new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey))); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("当前Java环境不支持RSA", e); } catch (InvalidKeySpecException e) { throw new RuntimeException("无效的密钥格式"); } } }3.注意事项以及相关说明3.1 参数组装说明 转账到微信零钱官方文档:https://pay.weixin.qq.com/docs/merchant/apis/batch-transfer-to-balance/transfer-batch/initiate-batch-transfer.html 请求由两部分组成:详细请求信息和请求头信息,前者不用多说,按照官方文档进行参数对应即可.后者请求头信息包含两个请求头:Wechatpay-Serial和Authorization,前者为证书序列号,可以直接从商户平台证书管理中查看,Authorization为签名信息.签名解释的稍微通俗一点,就是把要传递给接口的参数,进行加密,加密的这个动作就是签名,作用就是保障这条请求是你的请求是安全的. Authorization又分为两部分:认证类型(固定为WECHATPAY2-SHA256-RSA2048)和认证信息.认证信息由商户号、随机字符串、时间戳(单位秒)、证书序列号、签名信息组成,其中签名信息由请求方式、随机字符串、时间戳(单位秒)、请求路径(/v3/transfer/batches)、请求体加密组成.具体以官方文档为准.参数确实很多,不过代码里面都已经组装好了,只需要更换对应的配置信息即可.接入过程中会遇到很多问题,自己已经调通,很多坑已经踩过,按照上面的签名方式进行可以排除掉大部分可能出现错误原因.3.2.resource配置文件读取 首先说下和证书相关的三个文件:apiclient_cert.p12 商户证书,apiclient_cert.pem 商户证书相关加密文件,apiclient_key.pem 商户秘钥文件.后两者都是基于证书进行导出的.基本上都是商户证书用的多比如说支付或是退款,后两者用的不多。其中获取privateKey对象就是基于读取apiclient_key.pem实现.在springboot项目中配置文件一般放置在resource目录下,关于读取方式这里踩过坑,可以使用resourceLoader进行读取.具体实现参考上面代码.3.3 错误的签名,验签失败问题分析以及处理 接口本地测试的时候这个问题耗费的时间最多,也是大部分同学都会遇到的问题,看过一篇总结贴感觉不错,可以按照里面说的进行自查:验签失败原因分析.仔细对比之后验签失败的原因锁定在商户号、证书序列号、apiclient_key.pem三者是否匹配这个问题上。关于校验三者是否匹配,提供的检验方法是使用postman导入官方提供的测试脚本,具体操作可以参考:https://github.com/wechatpay-apiv3/wechatpay-postman-script按照步骤执行之后发现测试结果是认证失败,所以考虑如何将三者进行正确匹配。 简单交代下我的情况:apiclient_cert.p12和apiclient_key.pem都是之前的人交接过来的,由于线上支付和退款都正常在用,所以apiclient_cert.p12 证书序列号 商户号应该是正常的,那唯一有问题的可能就是apiclient_key.pem.apiclient_key.pem可以通过apiclient_cert.p12重新导出,毕竟如果更换apiclient_cert.p12 需要将线上的证书进行更换,成本较大. 使用apiclient_cert.p12生成apiclient_key.pem的方法是使用openssl.window 64位openssl下载地址后期补充.安装直接默认安装即可,不再展开. 安装好之后打开黑窗口,进入到apiclient_cert.p12所在目录(可以将原来的apiclient_key.pem重命名做备份),运行以下命令即可生成apiclient_key.pem:openssl pkcs12 -nodes -clcerts -in apiclient_cert.p12 -out apiclient_key.pem 如果提示输入密码,可以直接输入商户号.生成的文件中保留从-----BEGIN PRIVATE KEY-----到-----END PRIVATE KEY----内容即可,否则解析文件时会失败.生成新的apiclient_key.pem重新请求接口签名问题解决. 这里说下之前感到困惑的地方,官方提供过签名以及验签的工具,注意工具只校验签名的方式是否正确,不校验参数的正确性.问题记录贴:https://developers.weixin.qq.com/community/pay/doc/00004e6fa08528c447eea27cf568003.4.转账到零钱产品功能配置 商户平台开通转账到零钱产品只是第一步,需要到产品设置中进行开启api权限相关配置,设置比较简单这里不在展开.可能会出现问题的步骤下文会有记录,继续往下看.3.5.linux环境部署jar:cannot be resolved to absolute file path because it does not reside in the file system问题处理本地自测时一切正常,但是打包部署到线上之后接口异常,异常信息如下:cannot be resolved to absolute file path because it does not reside in the file system出现这个问题的原因是spring中不能通过file方式读取jar中内容信息.可以使用inputStream的方式进行读取.涉及修改的是sign方法:public String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, KeyStoreException, IOException { Signature sign = Signature.getInstance("SHA256withRSA"); // 存在问题: class path resource [config/cert/apiclient_key.pem] cannot be resolved to absolute file path because it does not reside in the file system: // 原因:打包之后,spring没办法通过File的形式访问jar包里面的文件。 /* Resource resource = resourceLoader.getResource('classpath:/config/cert/apiclient_key.pem'); File file = resource.getFile(); String path = file.getPath(); PrivateKey privateKey = getPrivateKey(path);*/ Resource resource = resourceLoader.getResource("classpath:/config/cert/apiclient_key.pem"); PrivateKey privateKey = PemUtil .loadPrivateKey(resource.getInputStream()); sign.initSign(privateKey); sign.update(message); return Base64.encodeBase64String(sign.sign()); }添加PemUtil.javapublic class PemUtil { public static PrivateKey loadPrivateKey(String privateKey) { privateKey = privateKey .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); try { KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("当前Java环境不支持RSA", e); } catch (InvalidKeySpecException e) { throw new RuntimeException("无效的密钥格式"); } } public static PrivateKey loadPrivateKey(InputStream inputStream) { ByteArrayOutputStream os = new ByteArrayOutputStream(2048); byte[] buffer = new byte[1024]; String privateKey; try { for (int length; (length = inputStream.read(buffer)) != -1; ) { os.write(buffer, 0, length); } privateKey = os.toString("UTF-8"); } catch (IOException e) { throw new IllegalArgumentException("无效的密钥", e); } return loadPrivateKey(privateKey); } public static X509Certificate loadCertificate(InputStream inputStream) { try { CertificateFactory cf = CertificateFactory.getInstance("X509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream); cert.checkValidity(); return cert; } catch (CertificateExpiredException e) { throw new RuntimeException("证书已过期", e); } catch (CertificateNotYetValidException e) { throw new RuntimeException("证书尚未生效", e); } catch (CertificateException e) { throw new RuntimeException("无效的证书", e); } } }修改之后重新上线问题处理.3.6.转账到零钱接口返回API通道未开启问题处理转账申请接口验签调通之后,可能会遇到API通道未开启的错误提示,原因是没有开启API发起功能,点击前往功能按钮,如果是未设置任何发起方式会提醒开通下面哪种发起方式,接口申请选择API发起转账.开启需要设置服务端的ip地址(注意本机ip无效,另外配置完成之后可能会有延迟).添加允许访问ip之后需要设置转账验密人,这个转账验密人的意思就是每发起一笔转账申请,验密人微信客户端会收到转账提醒消息,需要输入转账设置的密码和接收到的短信验证码方可转账成功.转账成功之后的到账截图:到这里基本发起转账申请以及零钱到账功能基本已经打通,3.7.转账到零钱免密额度开启功能使用说明 每笔转账申请需要验密人进行密码和短信验证码校验.很不方便,如何不进行验密人同意直接转正到零钱呢,对应的设置就是免密额度功能开启,只要在设置好的免密额度之内的转账申请不需要验密人同意即可到账零钱. 开启免密额度功能需要先设置允许的免密额度 然后开启安全医生功能,这里需要添加诊断链接,就是验证域名是否合法. 添加诊断链接步骤如下: 这里说明一下下载的验证文件放置位置,文档中说的是放置于配置域名的根目录.说下自己的项目部署结构,项目访问域名:https://A.com/distributionDev/前端项目部署在nginx,后端springboot项目访问地址:https://A.com:8083.测试页面verify_4658330f30affb076ec2f21940d4e4e8.html两种放置方式 放置于前端项目下: 访问链接:https://A.com/distributionDev/verify_4658330f30affb076ec2f21940d4e4e8.html 放置于服务端项目下: 访问链接(同添加的诊断链接):https://A.com:8083/verify_4658330f30affb076ec2f21940d4e4e8.html.这里官方给出的说明可能有所歧义(官方给出诊断链接路径不用访问到测试页面),按照https://A.com:8083/verify_4658330f30affb076ec2f21940d4e4e8.html添加时验证通过,仅填写https://A.com:8083提示:您绑定的诊断链接格式有误,请检查后重试! 关于服务端如何访问静态文件参考下文进行设置:springboot项目URL访问Linux上的指定文件夹的静态资源文件以及访问本地任意磁盘文件设置 与微信商户平台技术支持确认过,此处按照服务端配置测试页面的方式进行验证即可.安全医生功能开启之后免密功能开启!3.8.关于如何判断零钱是否到账的说明 转账申请提交成功之后如何判断前是否到账到零钱?官方给出的建议是按照如下流程进行处理:官方地址: https://pay.weixin.qq.com/docs/merchant/products/batch-transfer-to-balance/development.html简单说下需要做的三个步骤: 第一步就是发送提现到零钱的转账申请,这个之前都已经说过,不再进行重复. 第二步查询零钱转账是否完成. 这里通过商家批次单号查询批次单,接口返回transfer_batch中的状态为FINISHED时进行第三步接口请求.这一步需要注意的问题: 客户端需要按照指定时间间隔调用此接口,调用的时间间隔为10s-15min(一笔转账操作批量处理笔数越多所需等待时间越长),官方给出的接口请求文档中need_query_detail字段是必填,说一下true以及false的区别,true表示查询transfer_detail_list集合信息,false表示不查询.detail_status中接口文档为选填,但是不传递会报错,已经跟客服确认过,必传并且传递all即可.具体可参考代码.另外一个问题关于接口响应参数transfer_batch中batch_status字段说明以及处理方式:WAIT_PAY: 待付款确认。需要付款出资商户在商家助手小程序或服务商助手小程序进行付款确认;ACCEPTED:已受理。批次已受理成功,若发起批量转账的30分钟后,转账批次单仍处于该状态,可能原因是商户账户余额不足等。商户可查询账户资金流水,若该笔转账批次单的扣款已经发生,则表示批次已经进入转账中,请再次查单确认;PROCESSING:转账中。已开始处理批次内的转账明细单 FINISHED:已完成。批次内的所有转账明细单都已处理完成FINISHED:已完成。批次内的所有转账明细单都已处理完成CLOSED:已关闭。可查询具体的批次关闭原因确认 WAIT_PAY:只要商户平台设置免密金额大于单次最大提现金额就不会出这种状态; ACCEPTED、PROCESSING多算是中间状态,客户端接收到这种还需要继续调用 CLOSED这种状态一般为转账金额超过商户平台设置的免密金额后但是管理员在24小时之内没有进行密码验证导致转账申请关闭,只要只要商户平台设置免密金额大于单次最大提现金额就不会出这种状态; FINISHED客户端接收到这种状态值之后就可以调用第三步查询账单详情了。第三步查询提现转账账单详情信息 通过商家明细单号查询明细单,通过接口返回的detail_status状态为SUCCESS表示是转转账成功.这个步骤中如果返回状态为成功则添加给用户扣减余额的处理。 下面直接贴一下相关代码:第二步查询转账批次信息代码:controller:@ApiOperation(value = "查询商户转账批次状态,当发起商家转账到零钱请求受理成功之后,需等待10s-15min左右(批次内笔数越多所需等待时间越长)才可调用",notes = "WAIT_PAY: 待付款确认。需要付款出资商户在商家助手小程序或服务商助手小程序进行付款确认\n" + "ACCEPTED:已受理。批次已受理成功,若发起批量转账的30分钟后,转账批次单仍处于该状态,可能原因是商户账户余额不足等。商户可查询账户资金流水,若该笔转账批次单的扣款已经发生,则表示批次已经进入转账中,请再次查单确认\n" + "PROCESSING:转账中。已开始处理批次内的转账明细单\n" + "FINISHED:已完成。批次内的所有转账明细单都已处理完成\n" + "CLOSED:已关闭。可查询具体的批次关闭原因确认,返回FINISHED时表示执行成功调用findTransferDetail接口,PROCESSING以及ACCEPTED则重复调用该接口") @GetMapping("/findTransferBatches") @ApiImplicitParams({ @ApiImplicitParam(name = "outBatchNo", value = "商户转账编号", required = true, dataType = "String", paramType = "query",example = "1"), }) public ResultVo findTransferBatches(@NotBlank(message = "商户转账编号不允许为空!") String outBatchNo) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, KeyStoreException { String batchStatus = payService.findTransferBatches(outBatchNo); return ResultVoUtil.success(batchStatus); }service实现类:public String findTransferBatches(String outBatchNo) throws SignatureException, NoSuchAlgorithmException, KeyStoreException, InvalidKeyException, IOException { // 组装请求 String requestUrl = StrUtil.concat(true, Constants.TRANSFER_BATCHES_URL, outBatchNo,"?need_query_detail=true&detail_status=ALL"); // 组装authorization String authorization = buildAuthorization("",Constants.GET,requestUrl); // 发送请求 String returnMsg = HttpRequest.get(requestUrl) .header("Authorization", authorization) .execute().body(); if(StrUtil.isBlank(returnMsg)) throw new BussinessExcption("查询商家批次单号查询批次单信息失败:获取信息为空!"); JSONObject returnTransferBatches = JSON.parseObject(returnMsg); log.info("商户批次单号信息:{}",returnTransferBatches); JSONObject transferBatch = returnTransferBatches.getJSONObject("transfer_batch"); if(ObjectUtil.isNull(transferBatch)){ throw new BussinessExcption("查询商户批次单号信息失败:获取信息为空!"); } String batchStatus = transferBatch.getString("batch_status"); return batchStatus; }第三步查询账单详情信息代码:controller:@ApiOperation(value = "查询转账到零钱账单详情信息",notes = "转账申请接口请求完成之后调用,以此判断是否到账用户,接口如果正常返回说明支付成功") @PostMapping("/findTransferDetail") public ResultVo findTransferDetail(@RequestBody @Validated TransferDetailQueryDto transferDetailQueryDto) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, KeyStoreException { payService.findTransferDetail(transferDetailQueryDto); return ResultVoUtil.success(); }请求参数:@ApiModel("账单明细查询请求参数") @Data public class TransferDetailQueryDto implements Serializable { private static final long serialVersionUID = 8912898195432388263L; @ApiModelProperty(value = "商户系统内部区分转账批次单下不同转账明细单的唯一标识",example = "x23zy545Bd5436",dataType = "String") @NotBlank(message = "商户系统内部区分转账批次单下不同转账明细单不能为空!") private String out_batch_no; @ApiModelProperty(value = "商户系统内部的商家批次单号,在商户系统内部唯一",example = "2",dataType = "Integer") @NotBlank(message = "商户系统内部的商家批次单号不能为空!") private String out_detail_no; }实现类:@Override public void findTransferDetail(TransferDetailQueryDto transferDetailQueryDto) throws SignatureException, NoSuchAlgorithmException, KeyStoreException, InvalidKeyException, IOException { // 组装请求 String requestUrl = StrUtil.concat(true, "https://api.mch.weixin.qq.com/v3/transfer/batches", "/out-batch-no/", transferDetailQueryDto.getOut_batch_no(), "/details/out-detail-no/", transferDetailQueryDto.getOut_detail_no()); // 组装authorization String authorization = buildAuthorization("","GET",requestUrl); // 发送请求 String returnMsg = HttpRequest.get(requestUrl) .header("Authorization", authorization) .execute().body(); if(StrUtil.isBlank(returnMsg)) throw new BussinessExcption("查询转账详情信息失败:获取信息为空!"); JSONObject returnTransferDetail = JSON.parseObject(returnMsg); // 处理响应 String detail_status = returnTransferDetail.getString("detail_status"); if(!"SUCCESS".equals(detail_status)) { log.error("转账详情返回信息:{}",returnTransferDetail); throw new BussinessExcption("转账失败:请联系工作人员!"); } } /** * @Author: txm * @Description: * @Param: transferDtoStr:请求内容 * @Param: method:请求方法 * @Param: reqUrl:请求完整路径 * @return: java.lang.String * @Date: 2022/12/1 11:54 **/ private String buildAuthorization(String transferDtoStr,String method,String reqUrl) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, KeyStoreException { // 签名信息 HttpUrl httpUrl = HttpUrl.get(reqUrl); String tokenInfo=getToken(method,httpUrl,transferDtoStr); log.info("Authorization认证信息:{}",tokenInfo); String authType="WECHATPAY2-SHA256-RSA2048"; // 认证头信息 return StrUtil.concat(true, authType," ",tokenInfo); } /** * @Author: txm * @Description: 获取签名信息 * @Param: [method, url, body] * @return: java.lang.String * @Date: 2022/11/24 16:08 **/ public String getToken(String method, HttpUrl url, String body) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, KeyStoreException { String nonceStr = RandomUtil.randomString(26); long timestamp = System.currentTimeMillis() / 1000; String message = buildMessage(method, url, timestamp, nonceStr, body); log.info("签名串:{}",message); String signature = sign(message.getBytes("utf-8")); log.info("签名信息:{}",signature); return "mchid=\"" + "商户号" + "\"," + "serial_no=\"" + "证书序列号" + "\"," + "nonce_str=\"" + nonceStr + "\"," + "timestamp=\"" + timestamp + "\"," + "signature=\"" + signature + "\""; } /** * @Author: txm * @Description: 组装签名请求信息 * @Param: [method, url, timestamp, nonceStr, body] * @return: java.lang.String * @Date: 2022/11/24 16:09 **/ public String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) { String canonicalUrl = url.encodedPath(); if (url.encodedQuery() != null) { canonicalUrl += "?" + url.encodedQuery(); } return method + "\n" + canonicalUrl + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n"; } 根据detail_status状态是否为SUCCESS判断零钱是否到账. 以上是对接商户转账到零钱过程中的总结和相关注意事项说明,看到这里希望对你有所帮助,欢迎评论区留言交流遇到的问题.
一、前言 数据库性能监控可以说是十分重要,能否自行搭建环境实现像阿里云或是腾讯云那样直观的展示不同维度数据的功能?答案是肯定的。下面详细说明一下安装部署过程以及过程中出现的问题,希望对你有所帮助!二、部署过程与问题记录 首先介绍一下用到的三个开源程序: prometheus:普罗米修斯可以简单理解为一个监控工具,以时间为单位展示指定数据维度的变化趋势。数据信息从哪来?主要是依赖数据采集器,对于mysql数据采集使用的是mysqld_exporter。 grafana:主要用于可视化展示的监控软件,让数据监控更直观,支持多种仪表盘类型,就好比经常见的数据大屏,仪表盘就是各种展示形式。下面分别讲下三个如何下载和安装。1.prometheus下载与启动 官方下载地址:https://prometheus.io/download/ 本文主要讲解如何在windows上安装,所以选择windows版本,这里下载版本为:prometheus-2.40.1.windows-amd64.zip解压之后找到prometheus.exe直接进行双击即可启动。看到下面的内容说明启动成功:查看启动端口:浏览器中直接访问:localhost:90902.mysqld_exporter下载与启动 官方下载地址同上:https://prometheus.io/download/,这里下载的版本为:mysqld_exporter-0.14.0.windows-amd64.zip.解压之后需要添加my.cnf文件(注意:解压完成之后是不存在这个文件的,需要手动添加)。主要作用是配置数据库连接信息。my.cnf中添加内容如下:[client] user=root password=**** host=192.125.256.12 prot=3308启动方式:mysqld_exporter解压所在目录中执行(注意不是双击mysqld_exporter.exe启动):mysqld_exporter.exe --config.my-cnf=my.cnf访问地址:localhost:9104收集的mysql信息如下:prometheus中配置mysqld_exporter,两者进行关联,从prometheus.yml中最后位置添加mysqld_exporter。prometheus.yml源文件内容如下:# my global config global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s). # Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: # - alertmanager:9093 # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. rule_files: # - "first_rules.yml" # - "second_rules.yml" # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. scrape_configs: # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config. - job_name: "prometheus" # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: ["localhost:9090"] # 自定义添加mysql监控任务 - job_name: "mysql_monitor" # metrics_path defaults to '/metrics' # scheme defaults to 'http'.添加mysqld_exporter之后:# my global config global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s). # Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: # - alertmanager:9093 # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. rule_files: # - "first_rules.yml" # - "second_rules.yml" # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. scrape_configs: # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config. - job_name: "prometheus" # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: ["localhost:9090"] # 自定义添加mysql监控任务 - job_name: "mysql_monitor" # metrics_path defaults to '/metrics' # scheme defaults to 'http'. # 监控mysql采集器 static_configs: - targets: ["localhost:9104"]配置好之后重启prometheus,将prometheus.exe运行窗口关闭,双击prometheus.exe即可。mysqld_exporter重启方式同理。访问localhost:9090,选择targets,正常情况下应该是有两条任务。说下遇到的两个问题: 关闭prometheus.exe然后重新启动出现闪退。 出现这种情况一般是yml格式缩进问题,新增内容和原始文件的缩进保持一致即可. 访问localhost:8090,右边error中显示Get "http://localhost:9104/metrics": dial tcp: lookup localhost on 114.114.114.114:53: no such host 处理方式:将prometheus.yml中localhost修改为本机ip,也不要使用127.0.0.1.3.grafana下载与启动 下载地址:https://grafana.com/grafana/download,下载完成之后按照步骤安装即可,可以自定义安装目录,此处不做截图说明,安装完成之后访问:localhost:3000.这一步可能会出现问题,直接访问出现下图问题: 说下当时的原因: 本机3000端口被占用,所以访问失败,修改端口为3001。找到defaults.ini并打开,将从下面修改端口号即可。# The http port to use http_port = 3000 浏览器问题:电脑上安装的谷歌、360浏览器访问都失败,下载了一个狐火浏览器问题解决。当然也试过其他方式比如上面说的重启(找到grafana-server.exe双击)。 默认的用户名密码都是admin,登录之后会立即要求修改密码。重新登录之后设置数据源(按照截图步骤进行设置即可): 设置仪表板:输入常用的仪表板格式:https://grafana.com/grafana/dashboards/7362-mysql-overview导入成功之后就能监控各种维度信息。4.运行Prometheus + Granafa步骤总结启动普罗修斯米,双击prometheus.exe;启动mysql数据采集器,cmd进入到mysql数据采集器安装目录执行:mysqld_exporter.exe --config.my-cnf=my.cnf访问Granafa,浏览器访问:http://localhost:3001至此mysql数据监控环境搭建完成!
前言 最近新上线项目,决定启用新的阿里云服务器,服务端项目打包之后部署到服务器,项目正常启动,在阿里云控制台开放指定端口之后接口访问不通,这里记录一下出现的问题的原因以及处理方案.问题处理过程 首先说按照之前的新项目部署流程,服务端代码本地自测完成之后进行打包,部署到阿里云服务器,然后在阿里云控制台上将指定端口开放指定ip(一般测试环境都是对公司公网ip允许访问). 之前都是按照固有思维进行设置,从来没有想过为什么设置之后会生效以及还需要考虑哪些才会生效,这次出现问题算是给"不想不问"的思维逻辑上了一课! 首先说阿里云上部署的项目想被访问到,从安全组上开放指定端口开放指定端口肯定是必须的,但是安全组设置不是第一步,第一个需要考虑的是防火墙,对于阿里云服务器上部署的服务端应用,访问顺序如下: 接口请求最先触达的是防火墙,简单了解一下防火墙的作用.防火墙主要是借助硬件和软件的作用于内部和外部网络的环境间产生一种保护的屏障,从而实现对计算机不安全网络因素的阻断。只有在防火墙同意情况下,用户才能够进入计算机内,如果不同意就会被阻挡于外. 所以在服务端项目运行正常的情况下,对于安全组开放指定端口后如果服务端接口访问不通,首先要看下防火墙是否开启,如果防火墙开启之后需要看下防火墙是否对指定端口进行开放.注意这里的开放指定端口与安全组的设置没有关系.关于查看防火墙是否开启要看Linux系统的版本,版本不同使用的命令是不同的,下面简单罗列一下常见版本对应的命令(注意以下命令没有特殊说明均在任意目录下执行):CentOS 6以下:查看防火墙开启状态: service iptables status 开启防火墙 service iptables start 关闭防火墙 service iptables stopCentOS 7:查看防火墙开启状态: systemctl status firewalld 开启防火墙 systemctl start firewalld.service 关闭防火墙 systemctl stop firewalld.serviceubantu:查看防火墙开启状态: ufw status 开启防火墙 ufw enable 关闭防火墙 ufw disable 出现问题的阿里云服务器对应的Linux版本就是CentOS 7,查看了一下防火墙的状态是开启状态. 其中状态为active表示是开启状态,下面看下防火墙开放的端口有哪些.查看防火墙允许开放的端口 firewall-cmd --list-ports 发现没有启动项目端口号,对防火墙开放指定端口操作如下:防火墙对8091端口进行开放 firewall-cmd --add-port=8091/tcp --permanent开启之后需要重新加载使配置生效firewall-cmd --reload 重新访问接口发现接口访问正常.原来的阿里云服务器上进行部署项目只需要从安全组中开放指定端口是因为所在的阿里云服务器防火墙都是关闭状态.总结 对于平常开发中一定要多问一个为什么,有疑问才会考虑原理,任何事情明白原理之后就算遇到类似问题脑海中会第一时间会想到问题可能出现在哪些方面,平常总常说:“出来混总是要还的!”,这句话在我们开发这里也同样适用,感觉有收获或是赞成的欢迎点赞或留言!
1. 前言 在平常的开发中经常会碰到定时任务处理的场景,比如说用户进行商品购买,下单之后超过半个小时还未进行支付则自动取消该笔订单,订单支付状态由待支付变更为已支付;用户收到商品后,可以对商品评价,限制三天或是七天内没有做出评价则将评价入口关闭,订单状态变更为已关闭。这类指定时间之后进行的业务逻辑处理都可以纳入定时任务处理的范畴中。 关于定时任务处理的实现方案有很多种,基于springboot搭建的项目架构,大部分都是使用@Schedual,原因是配置简单、cron表达式支持丰富、灵活的定时时间。就拿订单超时取消功能来讲,定期查询数据库中待支付订单,超过订单创建指定时间的进行批量更新操作变更为已取消。当然万事都是利弊,这种方式缺点也很明显,需要频繁对数据库进行查询、更新操操作,无疑会浪费数据库资源,毕竟会做多余无效的查询。那有没有其他实现方案,不通过数据库轮询的方式处理。答案是肯定有,定时任务的解决方案有多种,这里结合项目实战介绍一下基于redission延迟队列的处理方案。2.实现过程2.1添加依赖<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.10.5</version> </dependency>2.2.redis配置spring: redis: database: 2 host: xxx.xxx.xxx.xxx password: ***** port: 63792.3 Redission配置类@Slf4j @Configuration public class RedissionConfig { private final String REDISSON_PREFIX = "redis://"; private final RedisProperties redisProperties; public RedissionConfig(RedisProperties redisProperties) { this.redisProperties = redisProperties; } @Bean public RedissonClient redissonClient() { Config config = new Config(); String url = REDISSON_PREFIX + redisProperties.getHost() + ":" + redisProperties.getPort(); config.useSingleServer() .setAddress(url) .setPassword(redisProperties.getPassword()) .setDatabase(redisProperties.getDatabase()) .setPingConnectionInterval(2000); config.setLockWatchdogTimeout(10000L); try { return Redisson.create(config); } catch (Exception e) { log.error("RedissonClient init redis url:{}, Exception:{}", url, e); return null; } } }2.4 自定义延时队列工具类@Slf4j @Component public class RedisDelayQueueUtil { @Autowired private RedissonClient redissonClient; /** * 添加延迟队列 * * @param value:队列值 * @param delay:延迟时间 * @param timeUnit:时间单位 * @param queueCode:队列键 * @param <T> */ public <T> boolean addDelayQueue(T value, long delay, TimeUnit timeUnit, String queueCode) { if (StringUtils.isBlank(queueCode) || Objects.isNull(value)) { return false; } try { // redission的阻塞队列 RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode); // redission的延时队列 RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque); // 延时队列添加数据 delayedQueue.offer(value, delay, timeUnit); //delayedQueue.destroy(); log.info("添加延时队列成功,队列键:{},队列值:{},延迟时间:{}", queueCode, value, timeUnit.toSeconds(delay) + "秒"); } catch (Exception e) { log.error("添加延时队列失败: {}", e.getMessage()); throw new RuntimeException("(添加延时队列失败)"); } return true; } /** * 获取延迟队列 * * @param queueCode * @param <T> */ public <T> T getDelayQueue(@NonNull String queueCode) throws InterruptedException { if (StringUtils.isBlank(queueCode)) { return null; } RBlockingDeque<Map> blockingDeque = redissonClient.getBlockingDeque(queueCode); // 将队列中放入的第一个元素取出 T value = (T) blockingDeque.poll(); return value; } /** * 删除指定队列中的消息 * @param o 指定删除的消息对象队列值(同队列需保证唯一性) * @param queueCode 指定队列键 */ public boolean removeDelayedQueue(Object o,String queueCode) { if (StringUtils.isBlank(queueCode) || Objects.isNull(o)) { return false; } RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode); RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque); boolean flag = delayedQueue.remove(o); //delayedQueue.destroy(); if(flag){ log.info("删除延时队列成功, 删除信息:{},延迟时间:{}", o,queueCode); } return flag; } }2.5 延时队列枚举 可以将定时处理的场景都加入这里面,这里只演示订单未支付自动取消。@Getter @NoArgsConstructor @AllArgsConstructor public enum RedisDelayQueueEnum { ORDER_PAYMENT_TIMEOUT("ORDER_PAYMENT_TIMEOUT","支付超时,自动取消订单", "orderPaymentTimeout"); /** * 延迟队列 Redis Key */ private String code; /** * 描述 */ private String name; /** * 延迟队列具体业务实现的 Bean * 可通过 Spring 的上下文获取 */ private String beanId; }2.6 延时队列执行器/** * @ClassName: RedisDelayQueueHandle * @Desc: 延迟队列执行器 * @Author: txm * @Date: 2022/10/19 21:27 **/ public interface RedisDelayQueueHandle<T> { void execute(T t); }2.7 订单超时执行器具体逻辑/** * @ClassName: OrderPaymentTimeout * @Desc: 订单支付超时处理 * @Author: txm * @Date: 2022/10/19 21:28 **/ @Component @Slf4j public class OrderPaymentTimeout implements RedisDelayQueueHandle<Map> { @Override public void execute(Map map) { log.info("订单支付超时延迟消息:{}", map); // TODO 订单支付超时,自动取消订单处理业务... } }2. 8 延迟队列监测项目启动之后就会执行/** * @ClassName: RedisDelayQueueRunner * @Desc: 启动延迟队列监测 * @Author: txm * @Date: 2022/10/19 21:29 **/ @Slf4j @Component public class RedisDelayQueueRunner implements CommandLineRunner { @Autowired private RedisDelayQueueUtil redisDelayQueueUtil; @Autowired private ApplicationContext context; @Autowired private ThreadPoolTaskExecutor ptask; ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 5, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1000),new ThreadFactoryBuilder().setNameFormat("order-delay-%d").build()); @Override public void run(String... args) throws Exception { ptask.execute(() -> { while (true){ try { RedisDelayQueueEnum[] queueEnums = RedisDelayQueueEnum.values(); for (RedisDelayQueueEnum queueEnum : queueEnums) { Object value = redisDelayQueueUtil.getDelayQueue(queueEnum.getCode()); if (value != null) { RedisDelayQueueHandle<Object> redisDelayQueueHandle = (RedisDelayQueueHandle<Object>)context.getBean(queueEnum.getBeanId()); executorService.execute(() -> {redisDelayQueueHandle.execute(value);}); } } TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { log.error("Redission延迟队列监测异常中断):{}", e.getMessage()); } } }); log.info("Redission延迟队列监测启动成功"); } }2.9 支付业务实现 这里模拟下单之后未支付订单自动取消;另外下单并支付成功的需要从回调处理中将延时队列中的信息清除。 支付控制类:@RequestMapping("/pay") @Validated @RestController @Api( tags = {"支付模块"}) public class PayController { @Autowired private PayServiceImpl payService; @ApiOperation("下单处理") @PostMapping("/placeOrder") public ResultVo placeOrder() throws IOException { payService.placeOrder(); return ResultVoUtil.success(); } @ApiOperation("支付回调处理") @PostMapping("/notifyOrder") public ResultVo notifyOrder() throws IOException { payService.notifyOrder(); return ResultVoUtil.success(); } }支付实现类:@Slf4j @Service public class PayServiceImpl implements PayService { private RedisDelayQueueUtil redisDelayQueueUtil = SpringUtils.getBean(RedisDelayQueueUtil.class); /** * @Author: txm * @Description: 下单处理 * @Param: [] * @return: void * @Date: 2022/10/19 21:36 **/ @Override public void placeOrder() { System.out.println("模拟下单处理完成"); Map<String, String> map1 = new HashMap<>(); map1.put("orderId", "100"); map1.put("remark", "支付超时,自动取消订单"); // 添加订单支付超时,自动取消订单延迟队列。延迟40秒钟取消订单 redisDelayQueueUtil.addDelayQueue(map1, 40, TimeUnit.SECONDS, RedisDelayQueueEnum.ORDER_PAYMENT_TIMEOUT.getCode()); } /** * @Author: txm * @Description: 订单回调处理 * @Param: [] * @return: void * @Date: 2022/10/19 22:05 **/ @Override public void notifyOrder() { System.out.println("模拟订单回调成功.............."); Map<String, String> map1 = new HashMap<>(); map1.put("orderId", "100"); map1.put("remark", "订单支付超时,自动取消订单"); // 删除延时队列中的消息信息 redisDelayQueueUtil.removeDelayedQueue(map1,RedisDelayQueueEnum.ORDER_PAYMENT_TIMEOUT.getCode()); } }下单成功之后,会添加到延时队列40秒之后订单取消模拟回调处理之后删除延时队列消息 以上基于redission延时队列处理定时任务的实战记录。看到这里如果感觉还不错欢迎点赞!大家平常都是使用的什么哪种实现方案,欢迎评论区留言交流!
1.问题描述以及原因分析 阿里云OSS上传图片功能很多人可能对实现过,正常情况下会返回https开头的图片地址.但是今天业务系统中运营人员反应上传的合同详情页面打开异常,看过服务端的日志之后发现用户上传的图片地址带有blob,线上返回异常图片地址如下: 造成这种的现象的原因就是服务端进行多个图片截取时处理异常,导致页面打开失败!现在说下问题处理方式.2.处理方式说明 咨询过官方客服,反馈说正常情况下oss上传图片不会存在返回类似于blob:XXX格式,提供的处理方式就是判断图片上传是否成功.这里阿里云OSS图片上传使用的是服务端sdk实现,先看下原始的图片上传逻辑:public String uploadImg(MultipartFile[] multipartFiles) { // 返回多张图片地址 String imgs=""; for (int i = 0; i < multipartFiles.length; i++) { try { String fileName = System.currentTimeMillis()+"_"+multipartFiles[i].getOriginalFilename(); String pathKey=filePath + fileName; // bucketName表示阿里云OSS存储的配置信息:bucketName ossClient.putObject("bucketName", pathKey, new ByteArrayInputStream(multipartFiles[i].getBytes())); if(i < multipartFiles.length-1){ imgs = imgs + aliyunConfig.getUrlPrefix() + pathKey+","; }else { imgs = imgs + aliyunConfig.getUrlPrefix() + pathKey; } } } catch (Exception e) { log.error("图片上传失败:{}",e.getMessage()); throw new Exception("图片上传失败:"+e.getMessage()); } } return imgs; } 关于图片地址是直接进行字符串拼接,没有和阿里云OSS服务进行请求交互,这里的意思可以理解为应用服务端接口请求完成,并不代表阿里云OSS服务端文件存储完成(客户端存在的情况可能是五花八门,无法全部复现),最稳妥的处理方式是请求一下阿里云OSS服务端判断已上传成功的文件与原始的文件是否相同,如果相同则认为是一次有效的上传操作.现在提供两种处理方式,可以根据情况进行选择(两种方式均展示核心上传逻辑).2.1根据上传返回响应状态// bucketName表示阿里云OSS存储的配置信息:bucketName PutObjectResult putObjectResult = ossClient.putObject("bucketName", pathKey, new ByteArrayInputStream(multipartFiles[i].getBytes())); if(putObjectResult.getResponse().getStatusCode()==200){ if(i < multipartFiles.length-1){ imgs = imgs + aliyunConfig.getUrlPrefix() + pathKey+","; }else { imgs = imgs + aliyunConfig.getUrlPrefix() + pathKey; } }注意sdk版本需要升级到:3.15.0,依赖如下:<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.15.0</version> </dependency>2.2调用GetObject接口获取下文件的大小// bucketName表示阿里云OSS存储的配置信息:bucketName ossClient.putObject(aliyunConfig.getBucketName(), pathKey, new ByteArrayInputStream(multipartFiles[i].getBytes())); ObjectMetadata objectMetadata = ossClient.getObject(aliyunConfig.getBucketName(), pathKey).getObjectMetadata(); // 判断已上传文件大小与原始上传文件大小是否相同 if (objectMetadata.getContentLength() == multipartFiles[i].getSize()) { if(i < multipartFiles.length-1){ imgs = imgs + aliyunConfig.getUrlPrefix() + pathKey+","; }else { imgs = imgs + aliyunConfig.getUrlPrefix() + pathKey; } } 以上是对于此问题的两种处理方式,这次处理之后经过一段时间测试暂时没有反馈异常的图片上传问题,如果感觉对你有帮助欢迎评论区留言或是点赞收藏!
前言 之前讲过如何利用公众号针对指定用户完成业务操作之后实时发送消息.就好比在线医院公众号中看病挂号,挂号预约成功之后微信列表中会新增一条关注的公众号预约成功消息.具体实现步骤可以看下文章如何实现:手把手教你微信公众号如何给指定用户发送消息提醒,有兴趣的可以看下。现在要从小程序中要加入引导关注公众号并授权的功能,用于使用公众号发送业务消息提醒,注意这里不是订阅消息。 参考过很多其他同学的实现方案,总感觉有些繁琐,并且接口有调用次数的限制。结合业务场景梳理了一下流程,并对具体的实现做了详细说明,希望对有同样需求的同学有所帮助,下面详细说下整个流程。业务流程说明 用户登录小程序进入到消息模块之后,对于没有关注公众号的用户或是已经关注公众号但是没有授权的用户显示引导关注公众号提示信息,已经关注公众号并授权处理的不显示(授权逻辑主要是指将用户关注公众号的openID与用户信息进行绑定,下文会详细讲)业务截图如下: 点击进入关注公众号页面,这里进入的是公众号发布的一篇公众号关注文章,业务截图如下: 没有关注公众号的进入到关注页面。 对于已关注公众号的用户,直接进入公众号聊天页面,设置默认回复为授权链接(进入到此页面的为已关注公众号但是没有授权的用户)。截图如下: 点击开通消息之后进入到授权页面进行授权即可,授权成功之后跳转到小程序完成授权操作。 以上是对小程序中引导关注公众号的流程梳理,下面说下如何进行实现,主要侧重于服务端讲解实现原理!实现方案说明 这里需要用户进入到小程序之后获取一下用户的unionId,unionId可以说是同一个用户在微信开放平台下各个产品中的唯一标识.openId在微信平台下每个产品是唯一的.两个都需要后端进行入表操作.unionId可以放到用户注册登录中实现.openId入表逻辑可以放到授权操作中(这里使用的是公众号的openID,因为官方提供的公众号发送消息接口中需要获取关注公众号用户的唯一标识openID).主要说下本文重点:如何判断用户是否关注过公众号.梳理逻辑如下: 判断的主要逻辑是看用户表信息中是否有维护过公众号的openId,如果有则说明之前关注过,但考虑到关注的用户可能会取消关注但是表中还保存着之前的公众号openId,所以这里调用官方的获取用户基本信息接口根据subscribe进行判断现在用户是否处于关注状态.subscribe表示用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号,拉取不到其余信息。官方接口截图如下: 这里再贴一下公众号网页授权流程:需要在授权操作中维护一下用户在公众号下面的唯一标识:openId.代码以及表结构实现用户表结构设计如下:CREATE TABLE `user` ( `user_id` bigint(20) NOT NULL AUTO_INCREMENT, `login` varchar(20) NOT NULL COMMENT '用户唯一标识', `union_id` varchar(30) NOT NULL COMMENT '用户unionId', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', `gzh_open_id` varchar(60) DEFAULT '' COMMENT '用户关注的公众号openId', PRIMARY KEY (`user_id`), UNIQUE KEY `unique` (`login`), UNIQUE KEY `unionId_unique` (`union_id`) ) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表'; 主要提供两个接口,一个是判断用户数是否关注公众号并授权,另一个是用户授权接口(主要逻辑是引导用户关注公众号后将公众号openid绑定用户信息入表)。现提供两个接口的实现逻辑。1.判断用户是否关注公众号并授权@ApiOperation(value = "判断用户是否关注公众号并授权:true表示关注且授权,false表示未关注或已关注未授权",notes = "返回true表示已关注公众号并已进行授权(用户信息中维护openId),页面不做引导处理.返回false表示没有关注公众号或是关注公众号之后没有授权,可引导关注并授权") @ApiImplicitParams({ @ApiImplicitParam(name = "unionId", value = "unionId", required = true, dataType = "Boolean", paramType = "query",example = "1")}) @GetMapping("/checkIsFollowGzg") public ResultVo checkIsFollowGzg(@NotBlank(message = "unionId不允许为空!") String unionId){ boolean returnFlag = userService.checkIsFollowGzg(unionId); return ResultVoUtil.success(returnFlag); }service层实现具体逻辑:@Override public boolean checkIsFollowGzg(String unionId) { boolean returnFlag=false; // 根据unionId查询用户关注公众号的openId User userBasicInfo = userMapper.findUserBasicInfo(unionId,null); if(ObjectUtil.isNull(userBasicInfo)) throw new BussinessExcption(ApiCode.SYSTEM_EXCEPTION.getMessage()); String gzhOpenId = userBasicInfo.getGzhOpenId(); if(StrUtil.isBlank(gzhOpenId)) return returnFlag; // 根据公众号的openId和accessToken判断用户是否关注(subscribe为1表示关注) // 获取微信认证信息 String wxgAccessToken = getWxgAccessToken(); if (checkIsGzhUer(gzhOpenId, wxgAccessToken)) return true; return false; } /** * @Author: txm * @Description: 获取公众号认证AccessToken * @Param: [] * @return: java.lang.String * @Date: 2022/10/5 10:47 **/ public String getWxgAccessToken() { String returnMsg=""; JSONObject accessTokenObject = null; String gzhAppId =jobConfig.getGzhAppId(); String gzhSecret=jobConfig.getGzhSecrect(); String requestUrl = StrUtil.format(Constants.GZH_ACCESS_TOKEN_URL, gzhAppId, gzhSecret); try { returnMsg=HttpUtil.get(requestUrl); accessTokenObject = JSON.parseObject(returnMsg); log.info("错误信息:{}",accessTokenObject); } catch (Exception e) { log.error("获取用户AccessToken认证信息失败:{}",e.getMessage()); } String accessToken = accessTokenObject.getString("access_token"); if(StrUtil.isBlank(accessToken)) throw new BussinessExcption("响应异常:获取accessToken信息为空!"); return accessToken; } /** * @Author: txm * @Description: 校验是否是关注过公众号用户,返回false表示没有关注公众号;返回true表示关注过公众号 * @Param: [gzhOpenId, wxgAccessToken] * @return: boolean * @Date: 2022/10/8 16:06 **/ private boolean checkIsGzhUer(String gzhOpenId, String wxgAccessToken) { // 根据获取公众号用户基本信息 String responBasicUserInfo=""; JSONObject basicUserInfo = null; String reqUrl = StrUtil.format(Constants.USER_BASIC_INFO_URL, wxgAccessToken, gzhOpenId); try { responBasicUserInfo= HttpUtil.get(reqUrl); basicUserInfo = JSON.parseObject(responBasicUserInfo); } catch (Exception e) { log.error("获取用户公众号基本信息失败:{}",e.getMessage()); } if(ObjectUtil.isNull(basicUserInfo)) throw new BussinessExcption(ApiCode.SYSTEM_EXCEPTION.getMessage()); // 用户是否订阅该公众号标识,1表示关注,值为0时,代表此用户没有关注该公众号,拉取不到其余信息。 if(StrUtil.isBlank(basicUserInfo.getString("subscribe")) || StrUtil.equals("0",basicUserInfo.getString("subscribe"),true)) return false; return true; }2.用户授权操作@ApiOperation("公众号授权操作:判断是否关注公众号:返回true表示已经关注公众号用户并绑定公众号openId,返回false表示没有关注公众号") @PostMapping("/getWxgUserInfo") public ResultVo getWxgUserInfo(@RequestBody @Validated WxgUserInfoDto wxgUserInfoDto){ userService.getWxgUserInfo(wxgUserInfoDto); return ResultVoUtil.success(); }service层实现逻辑:/** * @Author: txm * @Description: 授权处理 * @Param: [wxgUserInfoDto] * @return: void * @Date: 2022/10/5 9:08 **/ @Override public void getWxgUserInfo(WxgUserInfoDto wxgUserInfoDto) { // 网页授权根据code获取access_token JSONObject accessTokenInfo = getWxgAccessTokenInfo(wxgUserInfoDto.getCode()); String accessToken = accessTokenInfo.getString("access_token"); String openid = accessTokenInfo.getString("openid"); if(StrUtil.isBlank(accessToken)|| StrUtil.isBlank(openid)) throw new BussinessExcption("获取用户信息失败:accessToken或openId获取为空!"); // 根据公众号的openId和accessToken判断用户是否关注(subscribe为1表示关注) // 获取微信认证信息access_token String wxgAccessToken = getWxgAccessToken(); // 校验用户是否已经关注公众号,已经关注公众号需要查询是否维护过公众号的openId if (checkIsGzhUer(openid, wxgAccessToken)){ // 根据unionId查询用户是否已经维护过公众号openId,没有的话需要更新用户关注公众号的openId String login = wxgUserInfoDto.getLogin(); User userBasicInfo = userMapper.findUserBasicInfo(null,login); if(ObjectUtil.isNull(userBasicInfo)) throw new BussinessExcption(ApiCode.SYSTEM_EXCEPTION.getMessage()); // 更新用户公众号openId int i = userMapper.updateUserOpenId(openid, login); if(i == 0) throw new BussinessExcption("操作失败:更新信息为空!"); } } /** * @Author: txm * @Description: 获取AccessToken * @Param: [code] * @return: com.alibaba.fastjson.JSONObject * @Date: 2022/10/5 9:08 **/ public JSONObject getWxgAccessTokenInfo(String code) { String accessTokenInfo = ""; JSONObject accessTokenObject = null; String gzhAppId =jobConfig.getGzhAppId(); String gzhSecret=jobConfig.getGzhSecrect(); String reqUrl = StrUtil.format(Constants.ACCESS_TOKEN_URL, gzhAppId, gzhSecret, code); try { accessTokenInfo=HttpUtil.get(reqUrl); log.info("错误信息:{}",accessTokenInfo); accessTokenObject = JSON.parseObject(accessTokenInfo); } catch (Exception e) { log.error("获取用户AccessToken认证信息失败:{}",e.getMessage()); } return accessTokenObject; }用户信息操作接口:public interface UserMapper { // 根据unionId查询用户基本信息 User findUserBasicInfo(String unionId); }用户信息持久层配置文件:<!-- 获取用户基本信息,实体类暂不提供可自行自定义--> <select id="findUserBasicInfo" resultType="com.kawa.job.api.wanted.user.entity.User"> select user_id,login,gzh_open_id from job_user where union_id=#{unionId} limit 1 </select> 以上是处理小程序中引导关注公众号的处理方案,如果看完感觉有所帮助,欢迎评论区留言或点赞,每个赞美都是对自己最大的鼓励和支持!
背景 现有一个类似boss直聘的招聘小程序,求职端和招聘端可以根据身份进行切换.要求实现两个问题: 1.求职端或是招聘端上线时,如果有未读消息需要显示未读消息数; 2.求职端和招聘端同时在线时,求职端投递简历之后,要求招聘端能够实时显示有新投递简历的消息信息;招聘端发送面试邀请时,求职端消息列表中实时显示出面试要求的消息信息.处理方案梳理 对于第一个问题,可以在进入到小程序页面之后,服务端提供一个获取用户未读消息数据查询的接口. 对于第二个问题想到的处理方案是可以在小程序里面做一个轮询处理,间隔一定的时间去不断去调用获取用户消息列表接口.存在的问题就是请求中有大半是无用,浪费带宽和服务器资源。所以考虑的处理方案是使用websocket处理,websocket的优点是服务端可以主动向客户端发送消息.能处理客户端轮询查询带来的问题. 自己实现了springboot+websocket进行消息推送的简单demo,使用在线的websocket地址用作客户端进行测试.有同样需求的同学可以参考并加入到自己的业务逻辑中.下面说下实现过程;实现过程 主要展示服务端实现方案,前端只需要创建websocket连接,每种客户端创建连接的方式不同,这里不提供实现,测试的时候使用在线websocket测试网站作为客户端.服务端配置 需要引入的依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>websocket配置类:@Configuration public class WebSocketConfig { // 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }websocket服务端主要逻辑:@Component @Slf4j @Service @ServerEndpoint("/webSocket/{login}/{type}") // login表示用户唯一标识,type表示用户类型:1.求职身份;2.面试身份 public class WebSocketServer { //当前在线连接数 private static int onlineCount = 0; // 每个在线用户会创建一个WebSocketServer对象 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>(); // 存放所有在线的客户端 key为用户的唯一标识:login,value为每个会话连接 private static Map<String, Session> clients = new ConcurrentHashMap<>(); // private Session session; // 用户login private String login = ""; // 处理使用@Autowire注入为空的问题,使用静态变量处理 private static NewsMapper newsMapper= SpringUtils.getBean(NewsMapper.class); /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("login") String login, @PathParam("type") Integer type) { clients.put(login, session); // this.session = session; webSocketSet.add(this); //加入set中 this.login = login; addOnlineCount(); //在线数加1 try { // 查询用户未读消息数 Integer unReadMsg=0; List<Long> noReadingNewsIds = newsMapper.findNoReadingNewsId(type, login); if(CollectionUtil.isNotEmpty(noReadingNewsIds)){ unReadMsg=noReadingNewsIds.size(); } // 用户上线提醒 sendMessage("用户"+login+"已上线("+("1".equals(login) ? "求职者)":"招聘者)")+",未读消息数:"+unReadMsg,session); log.info("有新窗口开始监听用户详情id:" + login +",当前在线人数为:" + getOnlineCount()); } catch (IOException e) { log.error("websocket IO Exception"); } } /** * 连接关闭调用的方法 */ @OnClose public void onClose(Session session) { clients.remove(login); webSocketSet.remove(this); //从set中删除 subOnlineCount(); //在线数减1 log.info("释放的login为:"+login); log.info("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * @ Param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, Session session) { log.info("收到来自窗口" + login + "的信息:" + message); //群发消息 for (WebSocketServer item : webSocketSet) { try { item.sendMessage(message,session); } catch (IOException e) { e.printStackTrace(); } } } /** * @ Param session * @ Param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); } /** * 实现服务器主动推送 */ public void sendMessage(String message,Session session) throws IOException { if(session != null){ session.getBasicRemote().sendText(message); } } /** * 校验是否在线,在线需要返回用户session信息 */ public Session checkIsOnline(String login) throws IOException { for (String onLineLogin : clients.keySet()) { if(login.equals(onLineLogin)){ return clients.get(login); } } return null; } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() { return webSocketSet; } }模拟业务场景:求职端发送简历给求职者@RestController @RequestMapping("/webSocket") public class WebSocketServerController { @Autowired private WebSocketServer webSocketServer; @GetMapping("/serverToClient") public ResultVo sendServerToClient(String login) throws IOException { System.out.println("求职者给招聘者发送简历操作.............."); // 判断用户是否在线 Session session = webSocketServer.checkIsOnline(login); if(ObjectUtil.isNotNull(session)){ webSocketServer.sendMessage("求职者给招聘者发送简历,招聘者已接收",session); } return ResultVoUtil.success(); } }模拟客户端 这里客户端使用在线websocket测试网站,地址:http://www.websocket-test.com/,测试数据:login:1,userType:1 模拟求职端; login:2,userType:2 模拟招聘端;请求地址为websocket服务器所在项目ip以及端口和请求参数,本地项目参数如下:ws://172.16.0.131:8080/webSocket/1/1模拟用户1登录上线:模拟用户2登录上线:用户上线之后可以获取到未读消息数!模拟用户1求职者发送简历给用户2应聘者这里提供模拟接口:以上是模拟客户端,不刷新情况下可以接收到服务端消息. 以上是站内消息推送的实现方案,希望对有同样需求的同学有所帮助!
背景说明 现在的项目中需要对展示的数据进行脱敏处理,类似的场景很常见,比如说展示的手机号、银行卡、用户姓名等全部用***这类的特殊字符进行代替。我们的项目就需要将岗位展示列表中的用户岗位发布姓名全部用星号进行替换.最初的时候是入门级版本,几行代码就可以实现。后期对项目进行优化,考虑到脱敏的场景以及脱敏的形式可能会变化(比如说手机号脱敏要求前面三位后两位之外的进行脱敏,用户姓名要求全部脱敏处理),所以对原有逻辑进行了修改,于是有了进阶版实现方式,优点在于对脱敏业务与序列化处理进行解耦,便于拓展和后期维护,下面就具体说下处理过程,希望对有同样需求的同学有所帮助!入门级处理 场景:将岗位查询列表中的用户姓名进行全部脱敏处理,使用三个星号进行代替.直接上代码:实体类ShopPostInfo:public class ShopPostInfo implements Serializable { private static final long serialVersionUID = -2040496864515327697L; // 数据脱敏处理,用户名全部替换为*** @JsonSerialize(using = DesensitizationSerializer.class) private String userName; // 省略其他信息自定义脱敏处理器DesensitizationSerializer:public class DesensitizationSerializer extends JsonSerializer<String> { // 序列化处理 @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 脱敏成*** gen.writeString("***"); } }脱敏后截图: 可以看到用户的姓名都已经被处理成***了.到这里问题是解决了,但是后期再有其他的脱敏需求怎么办,比如说想对手机号进行脱敏处理,只想对前三位后两位之外的数字进行脱敏处理怎么办?重新定义一个手机号脱敏处理器?那以后需要维护的脱敏类会越来越多,显得会有所冗余,毕竟每种脱敏处理器中仅有脱敏策略不同,是否可以只专注于脱敏策略的实现,对于序列化的实现进行统一处理.基于上面的疑问就有了脱敏处理的进阶版,请往下看!进阶版处理自定义脱敏注解@Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @JsonSerialize(using = DesensitizationSerializer.class) public @interface DesensitizationAnnotation { Class<? extends DesensitizationStrategy> strategy(); }自定义脱敏注解序列化处理器public class DesensitizationSerializer extends JsonSerializer<String> implements ContextualSerializer { // 脱敏处理策略 private DesensitizationStrategy desensitizationStrategy; public DesensitizationSerializer() { } public DesensitizationSerializer(DesensitizationStrategy desensitizationStrategy) { this.desensitizationStrategy = desensitizationStrategy; } // 序列化核心处理 @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { int a=0; // 脱敏处理公共逻辑 String desensitizationValue = desensitizationStrategy.desensitizationHandle(value); // 序列化处理公共方案 gen.writeString(desensitizationValue); } // 设置自定义脱敏策略:desensitizationSerializer @Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { JsonSerializer<?> desensitizationSerializer = null; if(null == property) desensitizationSerializer = prov.findNullValueSerializer(property); if(!Objects.equals(property.getType().getRawClass(), String.class)) desensitizationSerializer = prov.findValueSerializer(property.getType(), property); if(Objects.equals(property.getType().getRawClass(), String.class)){ // JsonSerializer设置自定义脱敏策略 desensitizationSerializer = handleDesensitizationSerializer(desensitizationSerializer, property); } return desensitizationSerializer; } // 脱敏设置 private JsonSerializer<?> handleDesensitizationSerializer(JsonSerializer<?> desensitizationSerializer, BeanProperty beanProperty) { // 获取脱敏注解 DesensitizationAnnotation desensitizationJsonSerializer = beanProperty.getAnnotation(DesensitizationAnnotation.class); if (desensitizationJsonSerializer == null) desensitizationJsonSerializer = beanProperty.getContextAnnotation(DesensitizationAnnotation.class); // 设置脱敏实例,添加自定义脱敏策略 if (desensitizationJsonSerializer != null) { try { desensitizationSerializer = new DesensitizationSerializer(desensitizationJsonSerializer.strategy().newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } return desensitizationSerializer; } }实体类中添加序列化处理器public class ShopPostInfo implements Serializable { private static final long serialVersionUID = -2040496864515327697L; // modify by txm 2022/10/4 数据脱敏处理 // @JsonSerialize(using = DesensitizationSerializer.class) @DesensitizationAnnotation(strategy = FullSensitization.class) private String userName; // 省略其他信息 }全部脱敏处理策略:public class FullSensitization implements DesensitizationStrategy{ // 全脱敏处理 @Override public String desensitizationHandle(String oldValue) { return "***"; } }指定格式脱敏策略,这里指定只对用户姓名第一个字以外进行脱敏处理(暂时没想出更合理的脱敏场景,能理解要优化的点就行)public class PatternDeSensitization implements DesensitizationStrategy{ // 部分脱敏处理,首个字符处理,其余都脱敏.这里使用的hutool中的strUtil工具类,直接使用string原生的方法也可以. @Override public String desensitizationHandle(String oldValue) { return StrUtil.replace(oldValue,1,oldValue.length(),'*'); } }测试结果: 全部脱敏实现效果: 部分脱敏实现效果: 以上是关于数据脱敏的实现方式,其中根据业务需求做了一下结构优化,如果有相同的业务需求,看完希望对你有帮助,欢迎评论区留言点赞!
前言 最近的需求中要求在小程序中跳转h5项目,前端需要提供一下业务域名.简单记录一下配置的注意事项.希望对有相同需求的同学有所帮助.下面是小程序业务域名配置路径(开发-开发管理-业务域名): 问题1:业务域名是什么? 回答:业务域名是能访问h5项目的域名,不是小程序的域名. 问题2:业务域名配置有哪些注意事项? 回答:不支持额外端口的,比如说不支持:wx.kwxy.com:8089;只能是wx.kwxy.com。另外不允许添加额外的拼接路径,比如说不支持:wx.kwxy.com/wx,仅支持wx.kwxy.com。这里感觉确实不是很友好,很多公司里的域名有限,一般是一个域名下面不同的路径能支持多个项目访问。按照微信官方的配置要求要么使用新域名,要么项目部署变更,将原来不带其他路径的域名对应的项目修改为新的项目。 问题3:校验文件防止位置问题 答:配置业务域名页面,会显示如下内容:请下载校验文件,并将文件放置在域名根目录下. 这里的校验文件放置的问题很多人会有疑问.说下我的情况,我这边的域名是在阿里云申请,web项目都是部署在nginx上,域名根目录放置位置是说域名所对应的项目下面放置校验文件.按照实际开发情况说下如何部署.将项目部署成https可以访问的形式,然后在项目根目录下面放置校验文件即可. 项目部署位置:nginx配置文件重要内容:server { listen 80; server_name wx.kwxy.com; #将请求转成https rewrite ^(.*)$ https://$host$1 permanent; } server { #监听443端口 listen 443; #域名 server_name wx.kwxy.com; ssl on; #ssl证书的pem文件路径 ssl_certificate /usr/local/nginx/ssl_cert/kwxy.com.pem; #ssl证书的key文件路径 ssl_certificate_key /usr/local/nginx/ssl_cert/kwxy.com.key; # wx项目 location /{ root /staticresource/activity/wx; index index.html; } } 访问链接:wx.kwxy.com/校验文件.txt,能访问成功说明配置成功。添加页面直接点击保存业务域名即为配置完成. 问题4:如果想在nginx上部署别的项目进行访问如何设置?按照微信官网的配置要求,这里只能配置一个根域名,但是如果这个nginx服务器部署别的web项目应该如何配置呢,具体操作步骤可以参考:nginx部署之https访问按照不同路径访问不同项目
前言 最近做项目结构优化,前端项目都是部署在nginx上,想实现同一个端口可以访问多个前端项目.这样可以提高服务器的端口复用率,降低项目部署以及维护成本.根据平常的需求,用两台nginx服务器分别支持http、https同一端口访问不同项目。下面将配置方式以及相关注意事项做简单梳理,希望对有相同需求的同学有所帮助,尽量提升效率,专注业务开发!http方式同一端口访问不同项目 nginx安装步骤这里不在介绍,有安装需求的同学可以参考:超详细的linux部署nginx实战记录,直接介绍如何如何配置:22服务器的8099设置为访问多个项目,关键配置文件如下:server { listen 8099; server_name 100.100.100.22; # 修改root默认目录 root /data/www; include /etc/nginx/default.d/*.conf; # 项目A location /A{ alias /staticresource/kawa_two_activity/A; index index.html; } # 项目B location /B { alias /staticresource/kawa_two_activity/B; index index.html; } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } }项目部署位置:/staticresource/kawa_two_activity/A; /staticresource/kawa_two_activity/B;项目访问方式:100.100.100.22:8099/A 100.100.100.22:8099/Bhttps方式同一端口访问不同项目 平常的项目中有些三方平台考虑到安全性,会要求服务端提供的链接为https,所以需要支持https访问.nginx默认是不支持https访问的,开启的方式可以参考:nginx中如何开启https访问功能,这里不在讲述,直接看配置文件.实际上同http的部署方式.250服务器80端口部署多个项目.server { listen 80; server_name wx.kwxy.com; #将请求转成https rewrite ^(.*)$ https://$host$1 permanent; } server { #监听443端口 listen 443; #域名 server_name wx.kwxy.com; ssl on; #ssl证书的pem文件路径 ssl_certificate /usr/local/nginx/ssl_cert/kwxy.com.pem; #ssl证书的key文件路径 ssl_certificate_key /usr/local/nginx/ssl_cert/kwxy.com.key; # 项目A location /A{ alias /staticresource/activity/A; index index.html; } # 项目B location /B{ alias /staticresource/activity/B; index index.html; } }项目部署位置:/staticresource/activity/A; /staticresource/activity/B;项目访问方式:wx.kwxy.com/A wx.kwxy.com/B 实际生产中用两台nginx服务器分别支持http、https基本上能满足大部分需求,后期有时间会看一下如何在一个nginx上同时支持https和http。如果对你有所帮助,欢迎评论区留言点赞!
前言 消息提醒功能是提升用户满意度的最有效方式,基于微信聊天的消息提醒也是现在最常见的消息提醒方式之一,常见的业务场景:从医院里面进行挂号预约,预约成功或是快到预约时间会从微信聊天列表中显示对应的提醒;这种提醒的方式实际上是借助于微信公众号进行实现,当然只是针对于关注公众号的用户进行消息推送.本文主要介绍如何从头开始实现公众号消息推送.操作步骤1.创建服务号类型的公众号并进行认证 首先要创建公众号,注意创建的类型一定是服务号,模板消息发送功能只支持服务号,订阅号只能进行消息订阅功能;这里面有坑,不仔细的同学会发现没有申请模板消息的入口,根本原因就是所使用的公众号是订阅号而不是服务号;查看公众号是服务号还是订阅号的方式可以参考下图(公账号登录之后右上角账号信息中),如果是订阅号,模板消息发送功能就不用考虑了.另外一定要进行认证,才允许申请模板消息.认证入口在这里: 由于已经认证过,所以右边不显示认证入口.认证的流程是补充公司营业执照相关信息、公司对公账号打款、三方电话认证等。这里需要交300认证费的,做过认证的都知道很简单,感觉人家三百赚的真容易.当时周六提交的认证信息,财务还没对公打款,交完三百之后三方的认证电话就打过来了,下午就显示认证通过了,效率确实很快,哈哈哈哈!2.申请模板消息功能 认证审核通过之后可以添加模板消息功能.添加入口如下: 由于已经添加过,所以广告服务下面不显示,都显示到已开通栏目中了.如果没有开通过但是从广告与服务中找不到模板消息,大概率当前的公众号类型为订阅号. 添加模板信息需要选择两个行业分类,按照业务类型选择对应的分类即可,如果选项中没有符合要求的分类信息可以选择其他.然后简单描述一下添加模板消息的目的.审核速度还可以,基本上两个小时就通过了,跟之前腾讯客服的工作效率截然不同,更别说是周六提审的.怀疑这个审核是不是也委托给了三方.添加模板信息比较简单,这里就不进行截图了.3.添加消息模板 模板消息功能中我的模板会显示已添加的消息模板(注意模板id发送消息的时候会用到) 对于模板消息,腾讯提供了很多的消息模板进行选择,基本上涵盖当前大部分的消息提醒需求,直接按照内容进行搜索添加就可以,如果当前的业务消息提醒确定没有也可以进行添加,审核通过后会添加到模板库中,所有开发者都能使用.自定义添加模板消息入口(先搜索,搜索不到才会显示入口):4.添加模板消息发送接口4.1获取权限AccessToken 公众号支持的接口基本上都需要权限认证,所以首先要获取AccessToken.获取方式如下:public String getWxgAccessToken() { // 服务号的appid以及秘钥 String appid="服务号的appid"; String Wxgsecret="服务号的秘钥"; String requestUrl = StrUtil.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}", appid, Wxgsecret); String returnMsg = HttpUtil.get(requestUrl); cn.hutool.json.JSONObject responseJsonObject = JSONUtil.parseObj(returnMsg); if(ObjectUtil.isNull(responseJsonObject)) throw new Exception("响应异常:获取信息为空!"); String accessToken = responseJsonObject.getStr("access_token"); return accessToken; } 请求发送使用hutool中的HttpUtil,引入的依赖如下:<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.2.4</version> </dependency>4.2获取要发送的用户的openId这里不提供接口,使用官方在线的接口文档,获取方式如下:官方文档中有获取关注公众号的所有用户信息接口.调用入口如下:其中的openId中会显示所有关注公众号的用户openId.4.3给指定用户发送消息请求参数:public class WxgMessageDto { // 消息接收者openId @NotBlank(message = "微信公众平台不允许为空!") private String openId; // 认证token @NotBlank(message = "accessToken不允许为空!") private String accessToken; // 省略get/set }控制层:@PostMapping("/sendWxgMessage") public ApiResult sendWxgMessage(@RequestBody @Validated WxgMessageDto wxgMessageDto) throws Exception { smallProgressService.sendWxgMessage(wxgMessageDto); return ApiResult.ok(); }发送消息具体逻辑:public void sendWxgMessage(WxgMessageDto wxgMessageDto) { // 组装消息内容 String templateId="模板id"; // 模板id String url="https://www.baidu.com/"; // 跳转路径(小程序之外) String appid=""; // 小程序appid String pagepath=""; // 小程序跳转路径 String client_msg_id=""; // 防重入id String first="朋友,有一个邀请面试通知待查收"; // 副标题 String keyword1=""; // 关键词1 String remark="更多详情信息请点击查看"; // 备注 String keyword1Value="阿里巴巴济南分巴"; // 信息 String keyword2Value="山东金融数字产业园"; // 信息 String keyword3Value="18560152023"; // 信息 String keyword4Value="总是想把世界上最好的给你,却发现世界上最好的是你.........."; // 信息 String color=""; // 颜色 String messageStr="{\n" + " \"touser\":"+"\""+wxgMessageDto.getOpenId()+"\""+",\n" + " \"template_id\":"+"\""+templateId+"\""+",\n" + " \"url\":"+"\""+url+"\""+", \n" + " \"miniprogram\":{\n" + " \"appid\":"+"\""+appid+"\""+",\n" + " \"pagepath\":"+"\""+pagepath+"\""+"\n" + " },\n" + // " \"client_msg_id\":\"MSG_000001\",\n" + " \"data\":{\n" + " \"first\": {\n" + " \"value\":"+"\""+first+"\""+",\n" + " \"color\":\"#173177\"\n" + " },\n" + " \"keyword1\":{\n" + " \"value\":"+"\""+keyword1Value+"\""+",\n" + " \"color\":\"#173177\"\n" + " },\n" + " \"keyword2\": {\n" + " \"value\":"+"\""+keyword2Value+"\""+",\n" + " \"color\":\"#173177\"\n" + " },\n" + " \"keyword3\": {\n" + " \"value\":"+"\""+keyword3Value+"\""+",\n" + " \"color\":\"#173177\"\n" + " },\n" + " \"keyword4\": {\n" + " \"value\":"+"\""+keyword4Value+"\""+",\n" + " \"color\":\"#173177\"\n" + " },\n" + " \"remark\":{\n" + " \"value\":"+" \""+remark+"\""+",\n" + " \"color\":\"#173177\"\n" + " }\n" + " }\n" + " }"; // 发送消息 String accessToken=wxgMessageDto.getAccessToken(); String returnMsg = HttpUtil.post(StrUtil.format("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={}", accessToken), messageStr); cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(returnMsg); String errmsg = jsonObject.getStr("errmsg"); if(!StrUtil.equals("ok",errmsg)) throw new BusinessException("消息发送失败!"); }关于请求参数拼接直接使用的是官方给出的请求示例,注意里面传参的时候需要进行引号的转义.5.移动端查看是否接收到已发送消息测试发送消息接口:查看消息接收情况:
背景 平常的开发中经常会遇到调用三方接口的需求,实现方法可以说五花八门,考虑项目规范以及便于维护,最好使用统一的请求发送工具,hutool中的HttpUtil就是一个不错的选择.对常见的get、post请求都已经进行了完整封装。下面结合具体的业务请求说一下如何使用,看一下请求发送是否够香够顺滑! 首先贴一下hutool的官方依赖以及官方文档:<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.2.4</version> </dependency>官方文档地址:hutool官方链接get请求 微信公众平台中的接口基本上都需要权限认证,接口中都需要传递access_token,所以获取access_token就是第一步操作.下面官方给出的获取方式:好,下面就说明如何发送请求以及如何进行解析: 使用到的请求方式:HttpUtil.get("请求链接") 字符串拼接方式:StrUtil.format("拼接字符串模板", "参数1", "参数2");写法要比直接用+拼接更加优雅,另外日志打印也可以使用此方式! 响应参数解析方式:JSONObject resultJsonObject = JSONUtil.parseObj("响应信息"); String "响应信息values"= responseJsonObject.getStr("响应信息key");业务实现:// 获取微信公众平台的accessToken public String getWxgAccessToken() { String appid="公众号appid信息"; String wxgSecret="公众号秘钥信息"; // 拼接请求链接 String requestUrl = StrUtil.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}", appid, wxgSecret); // 发送请求 String returnMsg = HttpUtil.get(requestUrl); JSONObject responseJsonObject = JSONUtil.parseObj(returnMsg); if(ObjectUtil.isNull(responseJsonObject)) throw new Exception("响应异常:获取信息为空!"); String accessToken = responseJsonObject.getStr("access_token"); return accessToken; }Post请求 微信公众号中的服务号是支持给用户发送消息信息,现在有消息发送接口,官方文档如下: 此需求中要求发送post请求,请求参数上要有access_token,其余请求信息放到请求体中,具体的实现方式如下(偷懒直接把官方组装的格式复制了一下):// 发送模板消息 public void sendWxgTemplateMessage() { // 组装消息内容 String touser=""; // 接收者openId String templateId=""; // 模板id String url=""; // 跳转路径(小程序之外) String appid=""; // 小程序appid String pagepath=""; // 小程序跳转路径 String client_msg_id=""; // 防重入id String first="岗位申请成功!"; // 副标题 String keyword1=""; // 关键词1 String remark="祝你面试成功!"; // 备注 String value="销售精英"; // 信息 String color=""; // 颜色 String messageStr="{\n" + " \"touser\":"+touser+",\n" + " \"template_id\":"+templateId+",\n" + " \"url\":"+url+", \n" + " \"miniprogram\":{\n" + " \"appid\":"+appid+",\n" + " \"pagepath\":"+pagepath+"\n" + " },\n" + " \"client_msg_id\":\"MSG_000001\",\n" + " \"data\":{\n" + " \"first\": {\n" + " \"value\":"+first+",\n" + " \"color\":\"#173177\"\n" + " },\n" + " \"keyword1\":{\n" + " \"value\":"+value+",\n" + " \"color\":\"#173177\"\n" + " },\n" + " \"keyword2\": {\n" + " \"value\":\"39.8元\",\n" + " \"color\":\"#173177\"\n" + " },\n" + " \"keyword3\": {\n" + " \"value\":\"2014年9月22日\",\n" + " \"color\":\"#173177\"\n" + " },\n" + " \"remark\":{\n" + " \"value\":"+remark+",\n" + " \"color\":\"#173177\"\n" + " }\n" + " }\n" + " }"; // 发送消息 String accessToken="accessToken"; // 发送请求 String returnMsg = HttpUtil.post(StrUtil.format("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={}", accessToken), messageStr); // 请求参数解析 JSONObject jsonObject = JSONUtil.parseObj(returnMsg); // 获取errmsg,判断消息发送是否成功 String errmsg = jsonObject.getStr("errmsg"); if(!StrUtil.equals("ok",errmsg)) throw new Exception("消息发送失败!"); } 以上是常见请求hutool的调用方式,平常项目开发中如果没有特殊要求还是使用封装好的工具类最好,可以避免重复"造轮子",也便于项目维护.看到这里如果对你有帮助,欢迎点赞或评论!
背景说明 最近在做项目优化,关于阿里云视频上传方面一直存在视频上传过慢问题.由于之前采用的是服务端进行视频上传,主要的逻辑是客户端上传视频到服务端进行视频文件暂存,然后服务端上传视频到阿里云.上传过慢猜测是上传视频到服务端要受到带宽的影响,决定直接采用客户端(项目是web项目)直接上传到阿里云,不经过服务端这个环节,猜测上传速度会快一些.两种上传方案流程图如下:下面对客户端上传视频流程简要梳理,其中罗列服务端需要支持的api web客户端上传视频链接:https://help.aliyun.com/document_detail/52204.html,这里选择上传地址和凭证方式,也是官方推荐方式.简单对服务端涉及接口进行简单说明: CreateUploadVideo-获取音视频上传地址和凭证:主要作用是客户端上传api中需要服务端传递指定的上传凭证、videoId等信息。官方文档地址:https://help.aliyun.com/document_detail/436543.html注意返回的参数中官方文档中需要进行base解密,这里踩过坑,实际上不需要进行解密.另外上传的音视频源文件不需要绝对路径,直接用文件名.mp4即可,这里前端的小伙伴也踩过坑,不清楚怎么获取客户端盘符路径,文档的示例确实有误导. RefreshUploadVideo - 刷新视频上传凭证:主要作用视频文件上传超时后重新获取视频上传凭证。官方文档地址:https://help.aliyun.com/document_detail/436550.html GetUploadDetails - 获取媒体上传详情:主要作用:获取媒体上传详情(如上传时间、已上传比例、上传来源等信息),用于决定前端调用获取视频播放地址接口的触发时机.就是说客户端调用阿里云上传接口响应成功之后不代表视频上传成功,其中可能会在转码,所以需要调用此接口查询一下上传完成的状态、上传完成的百分比或是转码是否完成。官方地址:https://help.aliyun.com/document_detail/436548.html?spm=5176.8465980.help.dexternal.35f21450u0vJif&scm=20140722.S_help%40%40%E6%96%87%E6%A1%A3%40%40436548.S_os%2Bhot.ID_436548-RL_GetUploadDetails-LOC_consoleUNDhelp-OR_ser-V_2-P0_0接口判断逻辑如下:关于视频转码问题,如果代码中没有关于转码的设置,一般是音视频点播控制台中进行了设置,具体查看路径如下:对播放格式无特殊要求,可以不进行转码或是仅支持部分播放格式,原因是视频上传完成之后如果有预览的需求,进行转码的时间会很长。所以调用获取视频详情接口会有问题,并非视频上传失败,而是处于转码中。 GetPlayInfo - 获取音视频播放地址:主要作用是用于视频播放,如果上传视频支持转码操作,会返回标清、流畅、高清等格式的视频信息。调用顺序是在GetUploadDetails - 获取媒体上传详情之后。服务端相关代码客户端配置类:@Component @Data public class TrainConfig { // 视频相关参数 @Value("${aliyun.videoAccessKeyId}") private String videoAccessKeyId; @Value("${aliyun.videoAccessKeySecret}") private String videoAccessKeySecret; @Value("${aliyun.videoEndpoint}") private String videoEndpoint; @Bean public Client initUploadVideoClient() throws Exception { Config config = new Config() .setAccessKeyId(videoAccessKeyId) .setAccessKeySecret(videoAccessKeySecret); config.endpoint = videoEndpoint; Client client = new Client(config); return client; } }配置文件:application-dev.ymlaliyun: #视频上传相关参数 videoAccessKeyId: xxxxxxxxxxxxxxxxxx videoAccessKeySecret: xxxxxxxxxxxxxxxxxx videoEndpoint: vod.cn-xxx.aliyuncs.com视频上传逻辑:@RequestMapping("/upload") @RestController @Validated public class UploadFile { @Autowired private Client client; @ApiOperation("根据videoId获取视频信息(视频上传成功之后调用,响应参数说明:duration--视频时长,单位秒;playInfoList中playURL视频播放地址,支持根据不同清晰度获取)") @ApiImplicitParams({ @ApiImplicitParam(name = "videoId", value = "视频videoId", required = true, dataType = "String", paramType = "query",example = "1"), }) @GetMapping("/findVideoInfo") public ResultVo<com.aliyun.vod20170321.models.GetPlayInfoResponseBody> findVideoInfo(@NotBlank(message = "视频id不允许为空!") String videoId) throws Exception { com.aliyun.vod20170321.models.GetPlayInfoRequest getPlayInfoRequest = new GetPlayInfoRequest().setVideoId(videoId); RuntimeOptions runtime = new RuntimeOptions(); GetPlayInfoResponseBody getPlayInfoResponseBody=null; com.aliyun.vod20170321.models.GetPlayInfoResponse getPlayInfoResponse = client.getPlayInfoWithOptions(getPlayInfoRequest, runtime); if(ObjectUtil.isNotNull(getPlayInfoResponse)){ getPlayInfoResponseBody = getPlayInfoResponse.getBody(); } return ResultVoUtil.success(getPlayInfoResponseBody); } @ApiOperation("获取上传凭证(web客户端上传)") @ApiImplicitParams({ @ApiImplicitParam(name = "fileName", value = "文件所在路径,示例:test2.mp4", required = true, dataType = "String", paramType = "query",example = "1"), @ApiImplicitParam(name = "title", value = "视频标题", required = true, dataType = "String", paramType = "query",example = "1"), }) @GetMapping("/getUploadAuth") public ResultVo getUploadAuth(@NotBlank(message = "文件名不允许为空!") String fileName,@NotBlank(message = "视频标题不允许为空!")String title) throws Exception { CreateUploadVideoRequest createUploadVideoRequest = new CreateUploadVideoRequest().setFileName(fileName).setTitle(title); RuntimeOptions runtime = new RuntimeOptions(); CreateUploadVideoResponse uploadVideoWithOptions=null; try { uploadVideoWithOptions = client.createUploadVideoWithOptions(createUploadVideoRequest, runtime); } catch (Exception _error) { TeaException error = new TeaException(_error.getMessage(), _error); throw new BussinessExcption(error.message); } CreateUploadVideoResponseBody createUploadVideoResponseBody = uploadVideoWithOptions.getBody(); return ResultVoUtil.success(createUploadVideoResponseBody); } @ApiOperation("刷新上传凭证(web客户端上传)") @ApiImplicitParams({ @ApiImplicitParam(name = "videoId", value = "视频id", required = true, dataType = "String", paramType = "query",example = "1"), }) @GetMapping("/refreshUploadVideo") public ResultVo RefreshUploadVideo(@NotBlank(message = "videoId不允许为空!") String videoId) throws Exception { RefreshUploadVideoRequest refreshUploadVideoRequest = new RefreshUploadVideoRequest().setVideoId(videoId); RuntimeOptions runtime = new RuntimeOptions(); RefreshUploadVideoResponse refreshUploadVideoResponse=null; RefreshUploadVideoResponseBody refreshUploadVideoResponseBody=null; try { refreshUploadVideoResponse = client.refreshUploadVideoWithOptions(refreshUploadVideoRequest, runtime); } catch (Exception _error) { TeaException error = new TeaException(_error.getMessage(), _error); throw new BussinessExcption(error.message); } if(ObjectUtil.isNotNull(refreshUploadVideoResponse)){ refreshUploadVideoResponseBody = refreshUploadVideoResponse.getBody(); } return ResultVoUtil.success(refreshUploadVideoResponseBody); } @ApiOperation("获取视频上传详情信息(web客户端上传)Status为normal即为上传成功") @ApiImplicitParams({ @ApiImplicitParam(name = "videoId", value = "视频id", required = true, dataType = "String", paramType = "query",example = "1"), }) @GetMapping("/getUploadDetails") public ResultVo getUploadDetails(@NotBlank(message = "videoId不允许为空!") String videoId) throws Exception { com.aliyun.vod20170321.models.GetUploadDetailsRequest getUploadDetailsRequest = new com.aliyun.vod20170321.models.GetUploadDetailsRequest().setMediaIds(videoId); com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions(); GetUploadDetailsResponse uploadDetailsWithOptions=null; try { uploadDetailsWithOptions = client.getUploadDetailsWithOptions(getUploadDetailsRequest, runtime); } catch (Exception _error) { TeaException error = new TeaException(_error.getMessage(), _error); throw new BussinessExcption(error.message); } return ResultVoUtil.success(uploadDetailsWithOptions); } }相关依赖没有进行展示,可以直接从api调试控制台中进行复制,里面的依赖一定是最新的,官方文档更新很不及时,已经发现多次了,也算是踩过的坑!!!官方api调试地址:https://next.api.aliyun.com/api,可以直接下载项目复制最新的依赖信息. 以上是阿里云上传视频客户端上传流程,希望对有相同需求的小伙伴有所帮助!欢迎评论区留言交流!
问题背景 新申请的阿里云域名,下文都叫域名A.com吧,注册申请成功之后从DNS解析里面添加了一条记录. 项目接口文档测试环境地址为:服务器ip:8080/doc.html,直接使用此地址可以进行访问,但是修改为:job.域名A.com:8080/doc.html就不能访问了.处理过程1首先看一下域名是否能ping通.ping job.域名A.com发现可以.2阿里云安全组是否开放8080端口 8080端口已指定开放端口.3防火墙是否关闭 这里使用的Ubuntu版本,执行sudo ufw status 显示是关闭4阿里云工单求助 客服反馈将8080端口开放ip修改为0.0.0.0/0,重新访问显示 阿里云客服建议域名备案之后重试.至于为什么域名访问时要将安全组授权ip设置为0.0.0.0/0,原因是正常使用域名访问需要先解析到服务器的ip 地址,客户端发起的请求会发送到对应的ip ,如果服务器侧有防火墙等限制会直接阻断网络请求,需要放行使用。 使用之前申请过并且备案过的域名B进行同样设置之后访问正常,所以最终处理方式是进行备案处理. 以上是我遇到此问题的处理过程以及解决方式,如果有同样疑惑的小伙伴可以参考上述进行排查,场景不同当然还会有其他原因,欢迎评论区交流
2023年01月