Compare commits

...

23 Commits

Author SHA1 Message Date
Leo
f89ed66bcc 1 2025-12-08 15:27:52 +08:00
Leo
a893f3cd61 1 2025-12-05 23:02:02 +08:00
Leo
c2be15e3f5 1 2025-12-05 23:01:41 +08:00
Leo
8445b500ae 1 2025-12-05 22:35:51 +08:00
Leo
ee67d1ae8f Merge branch 'master' of https://git.van333.cn/CC/Jarvis_java 2025-12-04 15:47:42 +08:00
Leo
a20e92d7bf 1 2025-12-04 15:47:39 +08:00
69d1d91f5e 1 2025-12-04 14:48:18 +08:00
Leo
570fcb0b93 1 2025-11-29 23:39:37 +08:00
Leo
7fda3da9ed 1 2025-11-29 22:47:41 +08:00
Leo
e7687c8909 1 2025-11-29 22:35:06 +08:00
Leo
8e12076225 1 2025-11-10 21:49:45 +08:00
Leo
5b48727fb2 1 2025-11-10 18:43:27 +08:00
Leo
bb6c907cda 1 2025-11-10 18:43:18 +08:00
Leo
bdd33581f1 1 2025-11-09 00:00:41 +08:00
Leo
ef358cc6b3 1 2025-11-08 15:25:43 +08:00
Leo
e76c6d4451 羽绒服 2025-11-08 02:18:35 +08:00
31ecfa6a2f 1 2025-11-03 19:49:12 +08:00
127a5b71c6 1 2025-11-03 19:44:22 +08:00
1872908dae 1 2025-11-03 16:03:34 +08:00
efdb727e48 1 2025-11-03 15:46:21 +08:00
4af64b58d6 1 2025-11-03 15:38:07 +08:00
424cf37260 1 2025-11-03 15:30:33 +08:00
47fd91b948 1 2025-11-03 15:29:21 +08:00
16 changed files with 1916 additions and 294 deletions

View File

@@ -1,215 +1,9 @@
2025-09-09 11:14:53 [main] INFO cn.van.Application - Starting Application using Java 17.0.14 with PID 40944 (D:\Code\Code\jd\target\classes started by 80787 in D:\Code\Code\jd)
2025-09-09 11:14:53 [main] DEBUG cn.van.Application - Running with Spring Boot v3.1.5, Spring v6.0.13
2025-09-09 11:14:53 [main] INFO cn.van.Application - The following 1 profile is active: "dev"
2025-09-09 11:14:54 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-6666"]
2025-09-09 11:14:54 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
2025-09-09 11:14:54 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.15]
2025-09-09 11:14:54 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
2025-09-09 11:14:56 [main] WARN o.s.c.LocalVariableTableParameterNameDiscoverer - Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: cn.van.business.repository.JDOrderRepository
2025-09-09 11:14:57 [main] WARN o.s.c.LocalVariableTableParameterNameDiscoverer - Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: cn.van.business.repository.OrderRowRepository
2025-09-09 11:14:57 [main] WARN o.s.c.LocalVariableTableParameterNameDiscoverer - Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: cn.van.business.repository.SuperAdminRepository
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 初始化超级管理员
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:凡 wxid_ytpc72mdoskt22
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:源 wxid_yneqf1implxu12
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:琳 wxid_ytpc72mdoskt22
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:淑 wxid_ytpc72mdoskt22
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:妈 wxid_ytpc72mdoskt22
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:心 wxid_ytpc72mdoskt22
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:敏 wxid_ytpc72mdoskt22
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:梓豪 wxid_ytpc72mdoskt22
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:楠 wxid_ytpc72mdoskt22
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:星 wxid_sr3r8ot0z6do12
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员0213 wxid_cfmrk2upjtf322
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:轩 wxid_ytpc72mdoskt22
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:小怪兽 wxid_m5ibcpe0ukw521
2025-09-09 11:14:57 [main] INFO cn.van.business.util.WXUtil - 超级管理员:牛 wxid_ytpc72mdoskt22
2025-09-09 11:14:58 [main] INFO o.a.r.s.a.RocketMQAutoConfiguration - a producer (wx_producer) init on namesrv 192.168.8.88:39876
2025-09-09 11:15:03 [main] ERROR o.a.r.spring.core.RocketMQTemplate - syncSend failed. destination:wx-message, message:GenericMessage [payload={"data":{"msg":"[ 11:14:57 2025-09-09 ] \r\nJarvis 更新完成 [亲亲][亲亲][亲亲] ","wxid":"wxid_ytpc72mdoskt22","msgType":1,"fromWxid":"wxid_cfmrk2upjtf322","hiddenTime":false},"type":"sendText2"}, headers={TAGS=wx, id=e77aed6a-9420-8411-62ea-44dd39d7d1dc, timestamp=1757387699939}], detail exception info:
java.lang.IllegalStateException: org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to null failed
at org.apache.rocketmq.client.impl.factory.MQClientInstance.updateTopicRouteInfoFromNameServer(MQClientInstance.java:843)
at org.apache.rocketmq.client.impl.factory.MQClientInstance.updateTopicRouteInfoFromNameServer(MQClientInstance.java:574)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.tryToFindTopicPublishInfo(DefaultMQProducerImpl.java:887)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(DefaultMQProducerImpl.java:745)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:1564)
at org.apache.rocketmq.client.producer.DefaultMQProducer.send(DefaultMQProducer.java:475)
at org.apache.rocketmq.spring.core.RocketMQTemplate.syncSend(RocketMQTemplate.java:687)
at org.apache.rocketmq.spring.core.RocketMQTemplate.syncSend(RocketMQTemplate.java:487)
at org.apache.rocketmq.spring.core.RocketMQTemplate.syncSend(RocketMQTemplate.java:475)
at org.apache.rocketmq.spring.core.RocketMQTemplate.doSend(RocketMQTemplate.java:1142)
at org.apache.rocketmq.spring.core.RocketMQTemplate.doSend(RocketMQTemplate.java:61)
at org.springframework.messaging.core.AbstractMessageSendingTemplate.send(AbstractMessageSendingTemplate.java:109)
at cn.van.business.mq.MessageProducerService.sendMessage(MessageProducerService.java:54)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:699)
at cn.van.business.mq.MessageProducerService$$SpringCGLIB$$0.sendMessage(<generated>)
at cn.van.business.util.WXUtil.sendTextMessage(WXUtil.java:276)
at cn.van.business.util.WXUtil.initSuperAdmins(WXUtil.java:202)
at cn.van.business.util.WXUtil.<init>(WXUtil.java:107)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:211)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:110)
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:318)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:309)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1417)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:910)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:788)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1417)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:910)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:788)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:950)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:616)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:738)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:440)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295)
at cn.van.Application.main(Application.java:31)
Caused by: org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to null failed
at org.apache.rocketmq.remoting.netty.NettyRemotingClient.invokeSync(NettyRemotingClient.java:572)
at org.apache.rocketmq.client.impl.MQClientAPIImpl.getTopicRouteInfoFromNameServer(MQClientAPIImpl.java:2050)
at org.apache.rocketmq.client.impl.MQClientAPIImpl.getTopicRouteInfoFromNameServer(MQClientAPIImpl.java:2041)
at org.apache.rocketmq.client.impl.factory.MQClientInstance.updateTopicRouteInfoFromNameServer(MQClientInstance.java:782)
... 77 common frames omitted
2025-09-09 11:15:03 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'JDInnerController' defined in file [D:\Code\Code\jd\target\classes\cn\van\business\controller\jd\JDInnerController.class]: Unsatisfied dependency expressed through constructor parameter 1: Error creating bean with name 'JDUtil' defined in file [D:\Code\Code\jd\target\classes\cn\van\business\util\JDUtil.class]: Unsatisfied dependency expressed through constructor parameter 3: Error creating bean with name 'WXUtil' defined in file [D:\Code\Code\jd\target\classes\cn\van\business\util\WXUtil.class]: Failed to instantiate [cn.van.business.util.WXUtil]: Constructor threw exception
2025-09-09 11:15:06 [main] INFO o.a.catalina.core.StandardService - Stopping service [Tomcat]
2025-09-09 11:15:06 [main] ERROR o.s.boot.SpringApplication - Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'JDInnerController' defined in file [D:\Code\Code\jd\target\classes\cn\van\business\controller\jd\JDInnerController.class]: Unsatisfied dependency expressed through constructor parameter 1: Error creating bean with name 'JDUtil' defined in file [D:\Code\Code\jd\target\classes\cn\van\business\util\JDUtil.class]: Unsatisfied dependency expressed through constructor parameter 3: Error creating bean with name 'WXUtil' defined in file [D:\Code\Code\jd\target\classes\cn\van\business\util\WXUtil.class]: Failed to instantiate [cn.van.business.util.WXUtil]: Constructor threw exception
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:801)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:950)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:616)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:738)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:440)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295)
at cn.van.Application.main(Application.java:31)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'JDUtil' defined in file [D:\Code\Code\jd\target\classes\cn\van\business\util\JDUtil.class]: Unsatisfied dependency expressed through constructor parameter 3: Error creating bean with name 'WXUtil' defined in file [D:\Code\Code\jd\target\classes\cn\van\business\util\WXUtil.class]: Failed to instantiate [cn.van.business.util.WXUtil]: Constructor threw exception
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:801)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1417)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:910)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:788)
... 19 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'WXUtil' defined in file [D:\Code\Code\jd\target\classes\cn\van\business\util\WXUtil.class]: Failed to instantiate [cn.van.business.util.WXUtil]: Constructor threw exception
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:321)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:309)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1417)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:910)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:788)
... 33 common frames omitted
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [cn.van.business.util.WXUtil]: Constructor threw exception
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:224)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:110)
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:318)
... 47 common frames omitted
Caused by: org.springframework.messaging.MessagingException: org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to null failed
at org.apache.rocketmq.spring.core.RocketMQTemplate.syncSend(RocketMQTemplate.java:695)
at org.apache.rocketmq.spring.core.RocketMQTemplate.syncSend(RocketMQTemplate.java:487)
at org.apache.rocketmq.spring.core.RocketMQTemplate.syncSend(RocketMQTemplate.java:475)
at org.apache.rocketmq.spring.core.RocketMQTemplate.doSend(RocketMQTemplate.java:1142)
at org.apache.rocketmq.spring.core.RocketMQTemplate.doSend(RocketMQTemplate.java:61)
at org.springframework.messaging.core.AbstractMessageSendingTemplate.send(AbstractMessageSendingTemplate.java:109)
at cn.van.business.mq.MessageProducerService.sendMessage(MessageProducerService.java:54)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:699)
at cn.van.business.mq.MessageProducerService$$SpringCGLIB$$0.sendMessage(<generated>)
at cn.van.business.util.WXUtil.sendTextMessage(WXUtil.java:276)
at cn.van.business.util.WXUtil.initSuperAdmins(WXUtil.java:202)
at cn.van.business.util.WXUtil.<init>(WXUtil.java:107)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:211)
... 49 common frames omitted
Caused by: java.lang.IllegalStateException: org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to null failed
at org.apache.rocketmq.client.impl.factory.MQClientInstance.updateTopicRouteInfoFromNameServer(MQClientInstance.java:843)
at org.apache.rocketmq.client.impl.factory.MQClientInstance.updateTopicRouteInfoFromNameServer(MQClientInstance.java:574)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.tryToFindTopicPublishInfo(DefaultMQProducerImpl.java:887)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(DefaultMQProducerImpl.java:745)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:1564)
at org.apache.rocketmq.client.producer.DefaultMQProducer.send(DefaultMQProducer.java:475)
at org.apache.rocketmq.spring.core.RocketMQTemplate.syncSend(RocketMQTemplate.java:687)
... 71 common frames omitted
Caused by: org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to null failed
at org.apache.rocketmq.remoting.netty.NettyRemotingClient.invokeSync(NettyRemotingClient.java:572)
at org.apache.rocketmq.client.impl.MQClientAPIImpl.getTopicRouteInfoFromNameServer(MQClientAPIImpl.java:2050)
at org.apache.rocketmq.client.impl.MQClientAPIImpl.getTopicRouteInfoFromNameServer(MQClientAPIImpl.java:2041)
at org.apache.rocketmq.client.impl.factory.MQClientInstance.updateTopicRouteInfoFromNameServer(MQClientInstance.java:782)
... 77 common frames omitted
2025-11-03 15:29:34 [main] INFO cn.van.Application - Starting Application using Java 17.0.14 with PID 56676 (D:\code\jd\target\classes started by CC in D:\code\jd)
2025-11-03 15:29:34 [main] DEBUG cn.van.Application - Running with Spring Boot v3.1.5, Spring v6.0.13
2025-11-03 15:29:34 [main] INFO cn.van.Application - The following 1 profile is active: "dev"
2025-11-03 15:29:37 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-6666"]
2025-11-03 15:29:37 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
2025-11-03 15:29:37 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.15]
2025-11-03 15:29:37 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
2025-11-03 15:29:39 [main] ERROR o.s.o.j.LocalContainerEntityManagerFactoryBean - Failed to initialize JPA EntityManagerFactory: Unable to create index (originalUrl) on table 'image_conversions' since the column 'originalUrl' was not found (specify the correct column name, which depends on the naming strategy, and may not be the same as the entity property name)
2025-11-03 15:29:39 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create index (originalUrl) on table 'image_conversions' since the column 'originalUrl' was not found (specify the correct column name, which depends on the naming strategy, and may not be the same as the entity property name)

View File

@@ -0,0 +1,185 @@
package cn.van.business.controller;
import cn.van.business.model.ApiResponse;
import cn.van.business.service.MarketingImageService;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 营销图片合成控制器
* 提供营销图片生成的HTTP接口
*
* @author System
*/
@Slf4j
@RestController
@RequestMapping("/jarvis/marketing-image")
public class MarketingImageController {
@Autowired
private MarketingImageService marketingImageService;
/**
* 生成单张营销图片
*
* POST /jarvis/marketing-image/generate
*
* 请求体:
* {
* "productImageUrl": "商品主图URL",
* "originalPrice": 499.0,
* "finalPrice": 199.0,
* "productName": "商品名称(可选)"
* }
*
* 返回:
* {
* "code": 200,
* "msg": "操作成功",
* "data": {
* "imageBase64": "data:image/jpg;base64,..."
* }
* }
*/
@PostMapping("/generate")
public JSONObject generateMarketingImage(@RequestBody Map<String, Object> request) {
JSONObject response = new JSONObject();
try {
String productImageUrl = (String) request.get("productImageUrl");
Object originalPriceObj = request.get("originalPrice");
Object finalPriceObj = request.get("finalPrice");
String productName = (String) request.get("productName");
if (productImageUrl == null || originalPriceObj == null || finalPriceObj == null) {
response.put("code", 400);
response.put("msg", "缺少必要参数: productImageUrl, originalPrice, finalPrice");
return response;
}
Double originalPrice = parseDouble(originalPriceObj);
Double finalPrice = parseDouble(finalPriceObj);
if (originalPrice == null || finalPrice == null) {
response.put("code", 400);
response.put("msg", "价格参数格式错误");
return response;
}
String base64Image = marketingImageService.generateMarketingImage(
productImageUrl, originalPrice, finalPrice, productName);
Map<String, Object> data = new HashMap<>();
data.put("imageBase64", base64Image);
response.put("code", 200);
response.put("msg", "操作成功");
response.put("data", data);
} catch (Exception e) {
log.error("生成营销图片失败", e);
response.put("code", 500);
response.put("msg", "生成营销图片失败: " + e.getMessage());
}
return response;
}
/**
* 批量生成营销图片
*
* POST /jarvis/marketing-image/batch-generate
*
* 请求体:
* {
* "requests": [
* {
* "productImageUrl": "商品主图URL1",
* "originalPrice": 499.0,
* "finalPrice": 199.0,
* "productName": "商品名称1可选"
* },
* {
* "productImageUrl": "商品主图URL2",
* "originalPrice": 699.0,
* "finalPrice": 349.0,
* "productName": "商品名称2可选"
* }
* ]
* }
*
* 返回:
* {
* "code": 200,
* "msg": "操作成功",
* "data": {
* "results": [
* {
* "success": true,
* "imageBase64": "data:image/jpg;base64,...",
* "index": 0
* },
* {
* "success": false,
* "error": "错误信息",
* "index": 1
* }
* ],
* "total": 2,
* "successCount": 1,
* "failCount": 1
* }
* }
*/
@PostMapping("/batch-generate")
public JSONObject batchGenerateMarketingImages(@RequestBody Map<String, Object> request) {
JSONObject response = new JSONObject();
try {
@SuppressWarnings("unchecked")
List<Map<String, Object>> requests = (List<Map<String, Object>>) request.get("requests");
if (requests == null || requests.isEmpty()) {
response.put("code", 400);
response.put("msg", "请求列表不能为空");
return response;
}
Map<String, Object> result = marketingImageService.batchGenerateMarketingImages(requests);
response.put("code", 200);
response.put("msg", "操作成功");
response.put("data", result);
} catch (Exception e) {
log.error("批量生成营销图片失败", e);
response.put("code", 500);
response.put("msg", "批量生成营销图片失败: " + e.getMessage());
}
return response;
}
/**
* 解析Double值
*/
private Double parseDouble(Object value) {
if (value == null) {
return null;
}
if (value instanceof Double) {
return (Double) value;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
try {
return Double.parseDouble(value.toString());
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,176 @@
package cn.van.business.controller;
import cn.van.business.service.SocialMediaService;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 小红书/抖音内容生成控制器
*
* @author System
*/
@Slf4j
@RestController
@RequestMapping("/jarvis/social-media")
public class SocialMediaController {
@Autowired
private SocialMediaService socialMediaService;
/**
* 提取关键词
*
* POST /jarvis/social-media/extract-keywords
*
* {
* "productName": "商品名称"
* }
*/
@PostMapping("/extract-keywords")
public JSONObject extractKeywords(@RequestBody Map<String, Object> request) {
JSONObject response = new JSONObject();
try {
String productName = (String) request.get("productName");
if (productName == null || productName.trim().isEmpty()) {
response.put("code", 400);
response.put("msg", "商品名称不能为空");
return response;
}
Map<String, Object> result = socialMediaService.extractKeywords(productName);
response.put("code", 200);
response.put("msg", "操作成功");
response.put("data", result);
} catch (Exception e) {
log.error("提取关键词失败", e);
response.put("code", 500);
response.put("msg", "提取关键词失败: " + e.getMessage());
}
return response;
}
/**
* 生成文案
*
* POST /jarvis/social-media/generate-content
*
* {
* "productName": "商品名称",
* "originalPrice": 499.0,
* "finalPrice": 199.0,
* "keywords": "关键词1、关键词2",
* "style": "xhs" // xhs/douyin/both
* }
*/
@PostMapping("/generate-content")
public JSONObject generateContent(@RequestBody Map<String, Object> request) {
JSONObject response = new JSONObject();
try {
String productName = (String) request.get("productName");
Object originalPriceObj = request.get("originalPrice");
Object finalPriceObj = request.get("finalPrice");
String keywords = (String) request.get("keywords");
String style = (String) request.getOrDefault("style", "both");
if (productName == null || productName.trim().isEmpty()) {
response.put("code", 400);
response.put("msg", "商品名称不能为空");
return response;
}
Double originalPrice = parseDouble(originalPriceObj);
Double finalPrice = parseDouble(finalPriceObj);
Map<String, Object> result = socialMediaService.generateContent(
productName, originalPrice, finalPrice, keywords, style
);
response.put("code", 200);
response.put("msg", "操作成功");
response.put("data", result);
} catch (Exception e) {
log.error("生成文案失败", e);
response.put("code", 500);
response.put("msg", "生成文案失败: " + e.getMessage());
}
return response;
}
/**
* 一键生成完整内容(关键词 + 文案 + 图片)
*
* POST /jarvis/social-media/generate-complete
*
* {
* "productImageUrl": "商品主图URL",
* "productName": "商品名称",
* "originalPrice": 499.0,
* "finalPrice": 199.0,
* "style": "xhs"
* }
*/
@PostMapping("/generate-complete")
public JSONObject generateComplete(@RequestBody Map<String, Object> request) {
JSONObject response = new JSONObject();
try {
String productImageUrl = (String) request.get("productImageUrl");
String productName = (String) request.get("productName");
Object originalPriceObj = request.get("originalPrice");
Object finalPriceObj = request.get("finalPrice");
String style = (String) request.getOrDefault("style", "both");
if (productName == null || productName.trim().isEmpty()) {
response.put("code", 400);
response.put("msg", "商品名称不能为空");
return response;
}
Double originalPrice = parseDouble(originalPriceObj);
Double finalPrice = parseDouble(finalPriceObj);
Map<String, Object> result = socialMediaService.generateCompleteContent(
productImageUrl, productName, originalPrice, finalPrice, style
);
response.put("code", 200);
response.put("msg", "操作成功");
response.put("data", result);
} catch (Exception e) {
log.error("生成完整内容失败", e);
response.put("code", 500);
response.put("msg", "生成完整内容失败: " + e.getMessage());
}
return response;
}
/**
* 解析Double值
*/
private Double parseDouble(Object value) {
if (value == null) {
return null;
}
if (value instanceof Double) {
return (Double) value;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
try {
return Double.parseDouble(value.toString());
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,234 @@
package cn.van.business.controller;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 小红书/抖音提示词模板配置Controller
*
* @author System
*/
@Slf4j
@RestController
@RequestMapping("/jarvis/social-media/prompt")
public class SocialMediaPromptController {
@Autowired(required = false)
private StringRedisTemplate redisTemplate;
// Redis Key 前缀
private static final String REDIS_KEY_PREFIX = "social_media:prompt:";
// 模板键名列表
private static final String[] TEMPLATE_KEYS = {
"keywords",
"content:xhs",
"content:douyin",
"content:both"
};
// 模板说明
private static final Map<String, String> TEMPLATE_DESCRIPTIONS = new HashMap<String, String>() {{
put("keywords", "关键词提取提示词模板\n占位符%s - 商品名称");
put("content:xhs", "小红书文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
put("content:douyin", "抖音文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
put("content:both", "通用文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
}};
/**
* 获取所有提示词模板
*
* GET /jarvis/social-media/prompt/list
*/
@GetMapping("/list")
public JSONObject listTemplates() {
JSONObject response = new JSONObject();
try {
Map<String, Object> templates = new HashMap<>();
for (String key : TEMPLATE_KEYS) {
Map<String, Object> templateInfo = new HashMap<>();
templateInfo.put("key", key);
templateInfo.put("description", TEMPLATE_DESCRIPTIONS.get(key));
String template = getTemplateFromRedis(key);
templateInfo.put("template", template);
templateInfo.put("isDefault", template == null);
templates.put(key, templateInfo);
}
response.put("code", 200);
response.put("msg", "操作成功");
response.put("data", templates);
} catch (Exception e) {
log.error("获取提示词模板列表失败", e);
response.put("code", 500);
response.put("msg", "获取失败: " + e.getMessage());
}
return response;
}
/**
* 获取单个提示词模板
*
* GET /jarvis/social-media/prompt/{key}
*/
@GetMapping("/{key}")
public JSONObject getTemplate(@PathVariable String key) {
JSONObject response = new JSONObject();
try {
if (!isValidKey(key)) {
response.put("code", 400);
response.put("msg", "无效的模板键名");
return response;
}
String template = getTemplateFromRedis(key);
Map<String, Object> data = new HashMap<>();
data.put("key", key);
data.put("description", TEMPLATE_DESCRIPTIONS.get(key));
data.put("template", template);
data.put("isDefault", template == null);
response.put("code", 200);
response.put("msg", "操作成功");
response.put("data", data);
} catch (Exception e) {
log.error("获取提示词模板失败", e);
response.put("code", 500);
response.put("msg", "获取失败: " + e.getMessage());
}
return response;
}
/**
* 保存提示词模板
*
* POST /jarvis/social-media/prompt/save
*
* {
* "key": "keywords",
* "template": "提示词模板内容..."
* }
*/
@PostMapping("/save")
public JSONObject saveTemplate(@RequestBody Map<String, Object> request) {
JSONObject response = new JSONObject();
try {
String key = (String) request.get("key");
String template = (String) request.get("template");
if (!isValidKey(key)) {
response.put("code", 400);
response.put("msg", "无效的模板键名");
return response;
}
if (StrUtil.isBlank(template)) {
response.put("code", 400);
response.put("msg", "模板内容不能为空");
return response;
}
if (redisTemplate == null) {
response.put("code", 500);
response.put("msg", "Redis未配置无法保存模板");
return response;
}
String redisKey = REDIS_KEY_PREFIX + key;
redisTemplate.opsForValue().set(redisKey, template);
log.info("保存提示词模板成功: {}", key);
response.put("code", 200);
response.put("msg", "保存成功");
} catch (Exception e) {
log.error("保存提示词模板失败", e);
response.put("code", 500);
response.put("msg", "保存失败: " + e.getMessage());
}
return response;
}
/**
* 删除提示词模板(恢复默认)
*
* DELETE /jarvis/social-media/prompt/{key}
*/
@DeleteMapping("/{key}")
public JSONObject deleteTemplate(@PathVariable String key) {
JSONObject response = new JSONObject();
try {
if (!isValidKey(key)) {
response.put("code", 400);
response.put("msg", "无效的模板键名");
return response;
}
if (redisTemplate == null) {
response.put("code", 500);
response.put("msg", "Redis未配置无法删除模板");
return response;
}
String redisKey = REDIS_KEY_PREFIX + key;
redisTemplate.delete(redisKey);
log.info("删除提示词模板成功: {}", key);
response.put("code", 200);
response.put("msg", "删除成功,已恢复默认模板");
} catch (Exception e) {
log.error("删除提示词模板失败", e);
response.put("code", 500);
response.put("msg", "删除失败: " + e.getMessage());
}
return response;
}
/**
* 从 Redis 获取模板
*/
private String getTemplateFromRedis(String key) {
if (redisTemplate == null) {
return null;
}
try {
String redisKey = REDIS_KEY_PREFIX + key;
return redisTemplate.opsForValue().get(redisKey);
} catch (Exception e) {
log.warn("读取Redis模板失败: {}", key, e);
return null;
}
}
/**
* 验证模板键名是否有效
*/
private boolean isValidKey(String key) {
if (StrUtil.isBlank(key)) {
return false;
}
for (String validKey : TEMPLATE_KEYS) {
if (validKey.equals(key)) {
return true;
}
}
return false;
}
}

View File

@@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.*;
import java.util.Random;
import java.util.stream.Collectors;
@RestController
@@ -168,23 +169,43 @@ public class JDInnerController {
}
}
// 3⃣ 尝试使用已使用过的京东评论
if (commentToUse == null && !usedComments.isEmpty()) {
// 3⃣ 尝试使用已使用过的评论(随机从京东和淘宝中选择)
if (commentToUse == null) {
// 准备候选评论列表
List<Comment> candidateComments = new ArrayList<>();
List<String> candidateSources = new ArrayList<>(); // 记录来源,用于标识是京东还是淘宝
// 添加已使用过的京东评论
if (!usedComments.isEmpty()) {
Collections.shuffle(usedComments);
commentToUse = usedComments.get(0);
logger.info("使用已使用过的京东评论");
candidateComments.add(usedComments.get(0));
candidateSources.add("JD");
logger.info("已添加已使用过的京东评论到候选列表");
}
// 4⃣ 尝试使用已使用过的淘宝评论
if (commentToUse == null) {
// 添加已使用过的淘宝评论
String taobaoProductIdMap = tbMap.getOrDefault(productId, null);
if (taobaoProductIdMap != null && !taobaoProductIdMap.isEmpty()) {
logger.info("尝试获取已使用过的淘宝评论");
Comment taobaoComment = generateTaobaoComment(productType, true);
if (taobaoComment != null) {
commentToUse = taobaoComment;
candidateComments.add(taobaoComment);
candidateSources.add("TB");
logger.info("已添加已使用过的淘宝评论到候选列表");
}
}
// 如果候选列表不为空,随机选择
if (!candidateComments.isEmpty()) {
Random random = new Random();
int selectedIndex = random.nextInt(candidateComments.size());
commentToUse = candidateComments.get(selectedIndex);
String selectedSource = candidateSources.get(selectedIndex);
if ("TB".equals(selectedSource)) {
isTb = true;
logger.info("使用已使用过的淘宝评论");
logger.info("随机选择:使用已使用过的淘宝评论");
} else {
logger.info("随机选择:使用已使用过的京东评论");
}
}
}
@@ -399,6 +420,68 @@ public class JDInnerController {
}
}
/**
* 批量创建礼金券并生成包含礼金的推广链接
* 入参:{ skey, skuId/materialUrl, amount, quantity, batchSize, owner, skuName }
* 返回:{ results: [ {index, success, giftCouponKey, shortURL, error} ], total, successCount, failCount }
*/
@PostMapping("/batchCreateGiftCoupons")
public Object batchCreateGiftCoupons(@RequestBody Map<String, Object> body) {
String skey = body.get("skey") != null ? String.valueOf(body.get("skey")) : null;
if (checkSkey(skey)) {
return error("invalid skey");
}
String skuId = body.get("skuId") != null ? String.valueOf(body.get("skuId")) : null;
String materialUrl = body.get("materialUrl") != null ? String.valueOf(body.get("materialUrl")) : null;
String owner = body.get("owner") != null ? String.valueOf(body.get("owner")) : "g";
String skuName = body.get("skuName") != null ? String.valueOf(body.get("skuName")) : "";
double amount = parseDouble(body.get("amount"), 1.8);
int quantity = parseInt(body.get("quantity"), 1);
int batchSize = parseInt(body.get("batchSize"), 15);
String idOrUrl = skuId != null && !skuId.trim().isEmpty() ? skuId : materialUrl;
if (idOrUrl == null || idOrUrl.trim().isEmpty()) {
return error("skuId or materialUrl is required");
}
if (amount <= 0 || quantity <= 0) {
return error("amount and quantity must be positive");
}
if (batchSize <= 0 || batchSize > 100) {
return error("batchSize must be between 1 and 100");
}
logger.info("批量创建礼金券请求 - idOrUrl={}, amount={}, quantity={}, batchSize={}, owner={}, skuName={}",
idOrUrl, amount, quantity, batchSize, owner, skuName);
try {
List<Map<String, Object>> results = jdProductService.batchCreateGiftCouponsWithLinks(
idOrUrl, amount, quantity, batchSize, owner, skuName);
int successCount = 0;
int failCount = 0;
for (Map<String, Object> result : results) {
if (Boolean.TRUE.equals(result.get("success"))) {
successCount++;
} else {
failCount++;
}
}
JSONObject resp = new JSONObject();
resp.put("results", results);
resp.put("total", batchSize);
resp.put("successCount", successCount);
resp.put("failCount", failCount);
logger.info("批量创建礼金券完成 - 总数={}, 成功={}, 失败={}", batchSize, successCount, failCount);
return resp;
} catch (Exception e) {
logger.error("batchCreateGiftCoupons error", e);
return error("batchCreateGiftCoupons failed: " + e.getMessage());
}
}
/**
* 手动清理Redis中超过93天的旧数据
* 请求参数:{ skey }

View File

@@ -14,8 +14,8 @@ import java.time.LocalDateTime;
*/
@Entity
@Table(name = "image_conversions", indexes = {
@Index(name = "idx_original_url", columnList = "originalUrl"),
@Index(name = "idx_converted_url", columnList = "convertedUrl")
@Index(name = "idx_original_url", columnList = "original_url"),
@Index(name = "idx_converted_url", columnList = "converted_url")
})
@Data
public class ImageConversion {

View File

@@ -61,6 +61,12 @@ public class SuperAdmin {
@Column(name = "is_active", nullable = false)
private Integer isActive = 1;
/**
* 接收人企业微信用户ID多个用逗号分隔
*/
@Column(name = "touser", length = 500)
private String touser;
/**
* 创建时间
*/

View File

@@ -61,8 +61,9 @@ public class MessageConsumerService implements RocketMQListener<JSONObject> {
Integer msgType = data.getInteger("msgType");
String fromWxid = data.getString("fromWxid");
Boolean hiddenTime = data.getBoolean("hiddenTime");
String touser = data.getString("touser"); // 获取接收人参数
wxtsUtil.sendWxTextMessage(wxid, content, msgType, fromWxid, hiddenTime);
wxtsUtil.sendWxTextMessage(wxid, content, msgType, fromWxid, hiddenTime, touser);

View File

@@ -2,7 +2,6 @@ package cn.van.business.service;
import cn.van.business.model.pl.ImageConversion;
import cn.van.business.repository.ImageConversionRepository;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import lombok.extern.slf4j.Slf4j;
@@ -16,9 +15,7 @@ import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -97,22 +94,37 @@ public class ImageConvertService {
return Collections.emptyList();
}
log.info("开始转换图片URL列表共{}张图片", imageUrls.size());
List<String> convertedUrls = new ArrayList<>();
int successCount = 0;
int skipCount = 0;
for (String imageUrl : imageUrls) {
if (StrUtil.isBlank(imageUrl)) {
continue;
}
log.debug("处理图片URL: {}", imageUrl);
try {
String convertedUrl = convertImageUrl(imageUrl);
if (!convertedUrl.equals(imageUrl)) {
successCount++;
log.debug("图片转换成功: {} -> {}", imageUrl, convertedUrl);
} else {
skipCount++;
log.debug("图片无需转换非webp格式: {}", imageUrl);
}
convertedUrls.add(convertedUrl);
} catch (Exception e) {
// 转换失败时使用原URL不中断流程
skipCount++;
log.warn("图片转换失败使用原URL: {}. 错误: {}", imageUrl, e.getMessage());
convertedUrls.add(imageUrl);
}
}
log.info("图片URL转换完成共{}张,成功转换{}张,跳过/失败{}张",
imageUrls.size(), successCount, skipCount);
return convertedUrls;
}
@@ -128,31 +140,59 @@ public class ImageConvertService {
return originalUrl;
}
// 规范化URL处理协议相对URL//开头)
String normalizedUrl = normalizeUrl(originalUrl);
// 检查是否为webp格式
if (!isWebpFormat(originalUrl)) {
return originalUrl;
if (!isWebpFormat(normalizedUrl)) {
return originalUrl; // 返回原URL保持一致性
}
// 检查系统是否支持webp转换
if (!WebPImageIO.isWebPSupported()) {
log.debug("系统不支持webp格式跳过转换: {}", originalUrl);
log.warn("系统不支持webp格式跳过转换: {}", normalizedUrl);
throw new IOException("系统不支持webp格式转换");
}
// 检查是否已转换
Optional<ImageConversion> existing = imageConversionRepository.findByOriginalUrl(originalUrl);
// 使用规范化后的URL进行缓存查询和转换
// 检查是否已转换使用规范化URL作为key
Optional<ImageConversion> existing = imageConversionRepository.findByOriginalUrl(normalizedUrl);
if (existing.isPresent()) {
ImageConversion conversion = existing.get();
log.debug("使用缓存的转换结果: {} -> {}", originalUrl, conversion.getConvertedUrl());
log.debug("使用缓存的转换结果: {} -> {}", normalizedUrl, conversion.getConvertedUrl());
return conversion.getConvertedUrl();
}
// 执行转换
String convertedUrl = performConversion(originalUrl);
log.info("图片转换成功: {} -> {}", originalUrl, convertedUrl);
// 执行转换使用规范化URL
log.info("开始转换webp图片: {}", normalizedUrl);
String convertedUrl = performConversion(normalizedUrl);
log.info("图片转换成功: {} -> {}", normalizedUrl, convertedUrl);
return convertedUrl;
}
/**
* 规范化URL处理协议相对URL等特殊情况
*
* @param url 原始URL
* @return 规范化后的URL
*/
private String normalizeUrl(String url) {
if (StrUtil.isBlank(url)) {
return url;
}
// 清理特殊字符(如零宽字符)
String cleanUrl = url.trim().replaceAll("[\\u200B-\\u200D\\uFEFF]", "");
// 处理协议相对URL//开头)
if (cleanUrl.startsWith("//")) {
cleanUrl = "https:" + cleanUrl;
log.debug("转换协议相对URL: {} -> {}", url, cleanUrl);
}
return cleanUrl;
}
/**
* 检查URL是否为webp格式
*
@@ -164,13 +204,22 @@ public class ImageConvertService {
return false;
}
// 检查URL中是否包含.webp
String lowerUrl = url.toLowerCase();
// 清理URL中的特殊字符(如零宽字符)
String cleanUrl = url.trim().replaceAll("[\\u200B-\\u200D\\uFEFF]", "");
// 检查URL中是否包含.webp扩展名不区分大小写
String lowerUrl = cleanUrl.toLowerCase();
// 检查URL参数或路径中是否包含webp
return lowerUrl.contains(".webp") ||
boolean isWebp = lowerUrl.contains(".webp") ||
lowerUrl.contains("format=webp") ||
lowerUrl.contains("?webp") ||
lowerUrl.contains("&webp");
if (isWebp) {
log.debug("检测到webp格式图片: {}", cleanUrl);
}
return isWebp;
}
/**
@@ -307,7 +356,7 @@ public class ImageConvertService {
*/
private String generateFileName(String originalUrl) {
// 使用URL的MD5作为文件名避免重复和特殊字符问题
String md5 = cn.hutool.core.util.HashUtil.md5Hex(originalUrl);
String md5 = cn.hutool.crypto.digest.DigestUtil.md5Hex(originalUrl);
return md5 + ".jpg";
}

View File

@@ -0,0 +1,433 @@
package cn.van.business.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.van.business.util.ds.DeepSeekClientUtil;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* 营销图片合成服务
* 用于生成小红书等平台的营销对比图
*
* @author System
*/
@Slf4j
@Service
public class MarketingImageService {
@Autowired
private DeepSeekClientUtil deepSeekClientUtil;
// 输出图片尺寸
private static final int OUTPUT_WIDTH = 1080;
private static final int OUTPUT_HEIGHT = 1080;
// 字体配置(支持回退)
private static final String[] FONT_NAMES = {"Microsoft YaHei", "SimHei", "Arial", Font.SANS_SERIF}; // 字体优先级
private static final int ORIGINAL_PRICE_FONT_SIZE = 36; // 官网价字体大小
private static final int FINAL_PRICE_FONT_SIZE = 72; // 到手价字体大小
private static final int PRODUCT_NAME_FONT_SIZE = 32; // 商品名称字体大小
/**
* 获取可用字体
*/
private Font getAvailableFont(int style, int size) {
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
String[] availableFonts = ge.getAvailableFontFamilyNames();
for (String fontName : FONT_NAMES) {
for (String available : availableFonts) {
if (available.equals(fontName)) {
return new Font(fontName, style, size);
}
}
}
// 如果都不可用,使用默认字体
return new Font(Font.SANS_SERIF, style, size);
}
// 颜色配置
private static final Color ORIGINAL_PRICE_COLOR = new Color(153, 153, 153); // 灰色 #999999
private static final Color FINAL_PRICE_COLOR = new Color(255, 0, 0); // 红色 #FF0000
private static final Color PRODUCT_NAME_COLOR = new Color(51, 51, 51); // 深灰色 #333333
private static final Color BACKGROUND_COLOR = Color.WHITE; // 背景色
/**
* 生成营销图片
*
* @param productImageUrl 商品主图URL
* @param originalPrice 官网价
* @param finalPrice 到手价
* @param productName 商品名称可选如果为空则使用AI提取
* @return Base64编码的图片
*/
public String generateMarketingImage(String productImageUrl, Double originalPrice, Double finalPrice, String productName) {
try {
log.info("开始生成营销图片: productImageUrl={}, originalPrice={}, finalPrice={}, productName={}",
productImageUrl, originalPrice, finalPrice, productName);
// 1. 加载商品主图
BufferedImage productImage = loadProductImage(productImageUrl);
if (productImage == null) {
throw new IOException("无法加载商品主图: " + productImageUrl);
}
// 2. 提取商品标题关键部分(如果未提供)
String keyProductName = productName;
if (StrUtil.isBlank(keyProductName)) {
// 如果未提供商品名称,则无法提取,留空
keyProductName = "";
} else {
// 如果提供了完整商品名称,提取关键部分
keyProductName = extractKeyProductName(keyProductName);
}
// 3. 创建画布
BufferedImage canvas = new BufferedImage(OUTPUT_WIDTH, OUTPUT_HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = canvas.createGraphics();
// 设置抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
// 4. 绘制背景
g2d.setColor(BACKGROUND_COLOR);
g2d.fillRect(0, 0, OUTPUT_WIDTH, OUTPUT_HEIGHT);
// 5. 缩放并绘制商品主图(居中,保持比例)
int productImageSize = 800; // 商品图尺寸
int productImageX = (OUTPUT_WIDTH - productImageSize) / 2;
int productImageY = 80; // 顶部留白
BufferedImage scaledProductImage = Thumbnails.of(productImage)
.size(productImageSize, productImageSize)
.asBufferedImage();
g2d.drawImage(scaledProductImage, productImageX, productImageY, null);
// 6. 绘制商品名称(如果有)
int textStartY = productImageY + productImageSize + 40;
if (StrUtil.isNotBlank(keyProductName)) {
drawProductName(g2d, keyProductName, textStartY);
textStartY += 60; // 增加间距
}
// 7. 绘制官网价(带删除线,在上方)
int originalPriceY = textStartY + 80;
drawOriginalPrice(g2d, originalPrice, originalPriceY);
// 8. 绘制向下箭头
int arrowY = originalPriceY + 60;
drawDownArrow(g2d, arrowY);
// 9. 绘制到手价(大红色,在下方)
int finalPriceY = arrowY + 80;
drawFinalPrice(g2d, finalPrice, finalPriceY);
// 10. 绘制爆炸贴图装饰(右下角)
drawExplosionDecoration(g2d);
g2d.dispose();
// 11. 转换为Base64
String base64Image = imageToBase64(canvas, "jpg");
log.info("营销图片生成成功");
return base64Image;
} catch (Exception e) {
log.error("生成营销图片失败", e);
throw new RuntimeException("生成营销图片失败: " + e.getMessage(), e);
}
}
/**
* 加载商品主图
*/
private BufferedImage loadProductImage(String imageUrl) throws IOException {
try {
byte[] imageData = HttpUtil.downloadBytes(imageUrl);
if (imageData == null || imageData.length == 0) {
throw new IOException("下载图片失败或图片数据为空");
}
try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData)) {
return ImageIO.read(bais);
}
} catch (Exception e) {
log.error("加载商品主图失败: {}", imageUrl, e);
throw new IOException("加载商品主图失败: " + e.getMessage(), e);
}
}
/**
* 提取商品标题关键部分使用AI
*
* @param fullProductName 完整商品名称
* @return 提取的关键部分
*/
public String extractKeyProductName(String fullProductName) {
if (StrUtil.isBlank(fullProductName)) {
return "";
}
try {
// 使用DeepSeek提取商品标题关键部分
String prompt = String.format(
"请从以下商品标题中提取最关键的3-8个字作为核心卖点只返回提取的关键词不要其他内容\n%s",
fullProductName
);
String extracted = deepSeekClientUtil.getDeepSeekResponse(prompt);
if (StrUtil.isNotBlank(extracted)) {
// 清理可能的换行和多余空格
extracted = extracted.trim().replaceAll("\\s+", "");
// 限制长度
if (extracted.length() > 12) {
extracted = extracted.substring(0, 12);
}
log.info("提取商品标题关键部分成功: {} -> {}", fullProductName, extracted);
return extracted;
}
} catch (Exception e) {
log.warn("使用AI提取商品标题关键部分失败使用简单截取: {}", fullProductName, e);
}
// 降级方案:简单截取前部分
return simpleExtractKeyName(fullProductName);
}
/**
* 简单提取商品名称关键部分(降级方案)
*/
private String simpleExtractKeyName(String fullName) {
if (StrUtil.isBlank(fullName)) {
return "";
}
// 移除常见的规格信息如XL、175/96A等
String cleaned = fullName
.replaceAll("\\s*XL|L|M|S|XXL\\s*", "")
.replaceAll("\\s*\\d+/\\d+[A-Z]?\\s*", "")
.replaceAll("\\s*【.*?】\\s*", "")
.replaceAll("\\s*\\(.*?\\)\\s*", "");
// 提取前10-15个字符
if (cleaned.length() > 15) {
cleaned = cleaned.substring(0, 15);
}
return cleaned.trim();
}
/**
* 绘制商品名称
*/
private void drawProductName(Graphics2D g2d, String productName, int y) {
Font font = getAvailableFont(Font.BOLD, PRODUCT_NAME_FONT_SIZE);
g2d.setFont(font);
g2d.setColor(PRODUCT_NAME_COLOR);
// 计算文字宽度,如果太长则截断
FontMetrics fm = g2d.getFontMetrics();
String displayName = productName;
int maxWidth = OUTPUT_WIDTH - 80; // 左右各留40px
if (fm.stringWidth(displayName) > maxWidth) {
// 截断并添加省略号
while (fm.stringWidth(displayName + "...") > maxWidth && displayName.length() > 0) {
displayName = displayName.substring(0, displayName.length() - 1);
}
displayName += "...";
}
int textWidth = fm.stringWidth(displayName);
int x = (OUTPUT_WIDTH - textWidth) / 2; // 居中
g2d.drawString(displayName, x, y);
}
/**
* 绘制官网价(带删除线)
*/
private void drawOriginalPrice(Graphics2D g2d, Double originalPrice, int y) {
Font font = getAvailableFont(Font.BOLD, ORIGINAL_PRICE_FONT_SIZE);
g2d.setFont(font);
g2d.setColor(ORIGINAL_PRICE_COLOR);
String priceText = "官网价:¥" + String.format("%.0f", originalPrice);
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(priceText);
int x = (OUTPUT_WIDTH - textWidth) / 2; // 居中
// 绘制文字
g2d.drawString(priceText, x, y);
// 绘制删除线
int lineY = y - fm.getAscent() / 2;
g2d.setStroke(new BasicStroke(3.0f)); // 3px粗的删除线
g2d.drawLine(x, lineY, x + textWidth, lineY);
}
/**
* 绘制向下箭头
*/
private void drawDownArrow(Graphics2D g2d, int y) {
int centerX = OUTPUT_WIDTH / 2;
int arrowSize = 40;
g2d.setColor(new Color(200, 200, 200)); // 浅灰色箭头
g2d.setStroke(new BasicStroke(3.0f));
// 绘制竖线
g2d.drawLine(centerX, y, centerX, y + arrowSize);
// 绘制箭头(向下)
int[] xPoints = {centerX, centerX - 15, centerX + 15};
int[] yPoints = {y + arrowSize, y + arrowSize - 20, y + arrowSize - 20};
g2d.fillPolygon(xPoints, yPoints, 3);
}
/**
* 绘制到手价(大红色)
*/
private void drawFinalPrice(Graphics2D g2d, Double finalPrice, int y) {
Font font = getAvailableFont(Font.BOLD, FINAL_PRICE_FONT_SIZE);
g2d.setFont(font);
g2d.setColor(FINAL_PRICE_COLOR);
String priceText = "到手价:¥" + String.format("%.0f", finalPrice);
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(priceText);
int x = (OUTPUT_WIDTH - textWidth) / 2; // 居中
g2d.drawString(priceText, x, y);
}
/**
* 绘制爆炸贴图装饰(右下角)
*/
private void drawExplosionDecoration(Graphics2D g2d) {
// 绘制简单的爆炸形状(星形)
int centerX = OUTPUT_WIDTH - 120;
int centerY = OUTPUT_HEIGHT - 120;
int radius = 50;
g2d.setColor(new Color(255, 200, 0)); // 金黄色
g2d.setStroke(new BasicStroke(4.0f));
// 绘制星形爆炸效果
int points = 8;
int[] xPoints = new int[points * 2];
int[] yPoints = new int[points * 2];
for (int i = 0; i < points * 2; i++) {
double angle = Math.PI * i / points;
int r = (i % 2 == 0) ? radius : radius / 2;
xPoints[i] = (int) (centerX + r * Math.cos(angle));
yPoints[i] = (int) (centerY + r * Math.sin(angle));
}
g2d.fillPolygon(xPoints, yPoints, points * 2);
// 绘制内部圆形
g2d.setColor(new Color(255, 100, 0)); // 橙红色
g2d.fillOval(centerX - radius / 2, centerY - radius / 2, radius, radius);
}
/**
* 将BufferedImage转换为Base64字符串
*/
private String imageToBase64(BufferedImage image, String format) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, format, baos);
byte[] imageBytes = baos.toByteArray();
return "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(imageBytes);
}
/**
* 批量生成营销图片
*
* @param requests 批量请求列表
* @return 结果列表每个元素包含base64图片
*/
public Map<String, Object> batchGenerateMarketingImages(java.util.List<Map<String, Object>> requests) {
Map<String, Object> result = new HashMap<>();
java.util.List<Map<String, Object>> results = new java.util.ArrayList<>();
int successCount = 0;
int failCount = 0;
for (int i = 0; i < requests.size(); i++) {
Map<String, Object> request = requests.get(i);
Map<String, Object> itemResult = new HashMap<>();
try {
String productImageUrl = (String) request.get("productImageUrl");
Double originalPrice = getDoubleValue(request.get("originalPrice"));
Double finalPrice = getDoubleValue(request.get("finalPrice"));
String productName = (String) request.get("productName");
if (productImageUrl == null || originalPrice == null || finalPrice == null) {
throw new IllegalArgumentException("缺少必要参数: productImageUrl, originalPrice, finalPrice");
}
String base64Image = generateMarketingImage(productImageUrl, originalPrice, finalPrice, productName);
itemResult.put("success", true);
itemResult.put("imageBase64", base64Image);
itemResult.put("index", i);
successCount++;
} catch (Exception e) {
log.error("批量生成第{}张图片失败", i, e);
itemResult.put("success", false);
itemResult.put("error", e.getMessage());
itemResult.put("index", i);
failCount++;
}
results.add(itemResult);
}
result.put("results", results);
result.put("total", requests.size());
result.put("successCount", successCount);
result.put("failCount", failCount);
return result;
}
/**
* 安全获取Double值
*/
private Double getDoubleValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof Double) {
return (Double) value;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
try {
return Double.parseDouble(value.toString());
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,347 @@
package cn.van.business.service;
import cn.hutool.core.util.StrUtil;
import cn.van.business.util.ds.DeepSeekClientUtil;
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 小红书/抖音内容生成服务
* 提供关键词提取、文案生成等功能
*
* @author System
*/
@Slf4j
@Service
public class SocialMediaService {
@Autowired
private DeepSeekClientUtil deepSeekClientUtil;
@Autowired
private MarketingImageService marketingImageService;
@Autowired(required = false)
private StringRedisTemplate redisTemplate;
// Redis Key 前缀
private static final String REDIS_KEY_PREFIX = "social_media:prompt:";
// 默认提示词模板
private static final String DEFAULT_KEYWORDS_PROMPT =
"请从以下商品标题中提取3-5个最核心的关键词这些关键词要能突出商品的核心卖点和特色。\n" +
"要求:\n" +
"1. 每个关键词2-4个字\n" +
"2. 关键词要能吸引小红书/抖音用户\n" +
"3. 用逗号分隔,只返回关键词,不要其他内容\n" +
"商品标题:%s";
private static final String DEFAULT_CONTENT_PROMPT_XHS =
"请为小红书平台生成一篇商品推广文案,要求:\n" +
"1. 风格:真实、种草、有温度\n" +
"2. 开头用emoji或感叹句吸引注意\n" +
"3. 内容:突出商品亮点、使用场景、性价比\n" +
"4. 结尾:引导行动(如:快冲、闭眼入等)\n" +
"5. 长度150-300字\n" +
"6. 适当使用emoji和换行\n" +
"\n商品信息\n" +
"商品名称:%s\n" +
"%s" + // 价格信息
"%s" + // 关键词
"\n请直接生成文案内容不要添加其他说明";
private static final String DEFAULT_CONTENT_PROMPT_DOUYIN =
"请为抖音平台生成一篇商品推广文案,要求:\n" +
"1. 风格:直接、有冲击力、吸引眼球\n" +
"2. 开头:用疑问句或对比句抓住注意力\n" +
"3. 内容:强调价格优势、限时优惠、稀缺性\n" +
"4. 结尾:制造紧迫感,引导立即行动\n" +
"5. 长度100-200字\n" +
"6. 使用短句,节奏感强\n" +
"\n商品信息\n" +
"商品名称:%s\n" +
"%s" + // 价格信息
"%s" + // 关键词
"\n请直接生成文案内容不要添加其他说明";
private static final String DEFAULT_CONTENT_PROMPT_BOTH =
"请生成一篇适合小红书和抖音平台的商品推广文案,要求:\n" +
"1. 风格:真实、有吸引力\n" +
"2. 突出商品亮点和价格优势\n" +
"3. 长度150-250字\n" +
"\n商品信息\n" +
"商品名称:%s\n" +
"%s" + // 价格信息
"%s" + // 关键词
"\n请直接生成文案内容不要添加其他说明";
/**
* 提取商品标题关键词
*
* @param productName 商品名称
* @return 关键词列表
*/
public Map<String, Object> extractKeywords(String productName) {
Map<String, Object> result = new HashMap<>();
if (StrUtil.isBlank(productName)) {
result.put("success", false);
result.put("error", "商品名称不能为空");
return result;
}
try {
// 从 Redis 读取提示词模板,如果没有则使用默认模板
String promptTemplate = getPromptTemplate("keywords", DEFAULT_KEYWORDS_PROMPT);
String prompt = String.format(promptTemplate, productName);
String response = deepSeekClientUtil.getDeepSeekResponse(prompt);
if (StrUtil.isNotBlank(response)) {
// 解析关键词
String[] keywords = response.trim()
.replaceAll("[,]", ",")
.split(",");
List<String> keywordList = new ArrayList<>();
for (String keyword : keywords) {
String cleaned = keyword.trim();
if (StrUtil.isNotBlank(cleaned) && cleaned.length() <= 6) {
keywordList.add(cleaned);
}
}
// 限制数量
if (keywordList.size() > 5) {
keywordList = keywordList.subList(0, 5);
}
result.put("success", true);
result.put("keywords", keywordList);
result.put("keywordsText", String.join("", keywordList));
log.info("提取关键词成功: {} -> {}", productName, keywordList);
} else {
throw new Exception("AI返回结果为空");
}
} catch (Exception e) {
log.error("提取关键词失败", e);
result.put("success", false);
result.put("error", "提取关键词失败: " + e.getMessage());
// 降级方案:简单提取
result.put("keywords", simpleExtractKeywords(productName));
result.put("keywordsText", String.join("", simpleExtractKeywords(productName)));
}
return result;
}
/**
* 生成小红书/抖音文案
*
* @param productName 商品名称
* @param originalPrice 原价
* @param finalPrice 到手价
* @param keywords 关键词(可选)
* @param style 文案风格xhs小红书、douyin抖音、both通用
* @return 生成的文案
*/
public Map<String, Object> generateContent(String productName, Double originalPrice,
Double finalPrice, String keywords, String style) {
Map<String, Object> result = new HashMap<>();
if (StrUtil.isBlank(productName)) {
result.put("success", false);
result.put("error", "商品名称不能为空");
return result;
}
try {
// 构建价格信息
StringBuilder priceInfo = new StringBuilder();
if (originalPrice != null && originalPrice > 0) {
priceInfo.append("原价:¥").append(String.format("%.0f", originalPrice)).append("\n");
}
if (finalPrice != null && finalPrice > 0) {
priceInfo.append("到手价:¥").append(String.format("%.0f", finalPrice)).append("\n");
}
// 构建关键词信息
String keywordsInfo = "";
if (StrUtil.isNotBlank(keywords)) {
keywordsInfo = "关键词:" + keywords + "\n";
}
// 从 Redis 读取提示词模板,如果没有则使用默认模板
String promptTemplate;
if ("xhs".equals(style)) {
promptTemplate = getPromptTemplate("content:xhs", DEFAULT_CONTENT_PROMPT_XHS);
} else if ("douyin".equals(style)) {
promptTemplate = getPromptTemplate("content:douyin", DEFAULT_CONTENT_PROMPT_DOUYIN);
} else {
promptTemplate = getPromptTemplate("content:both", DEFAULT_CONTENT_PROMPT_BOTH);
}
String prompt = String.format(promptTemplate, productName, priceInfo.toString(), keywordsInfo);
String content = deepSeekClientUtil.getDeepSeekResponse(prompt.toString());
if (StrUtil.isNotBlank(content)) {
result.put("success", true);
result.put("content", content.trim());
log.info("生成文案成功: {}", productName);
} else {
throw new Exception("AI返回结果为空");
}
} catch (Exception e) {
log.error("生成文案失败", e);
result.put("success", false);
result.put("error", "生成文案失败: " + e.getMessage());
// 降级方案:生成简单文案
result.put("content", generateSimpleContent(productName, originalPrice, finalPrice));
}
return result;
}
/**
* 一键生成完整内容(关键词 + 文案 + 图片)
*
* @param productImageUrl 商品主图URL
* @param productName 商品名称
* @param originalPrice 原价
* @param finalPrice 到手价
* @param style 文案风格
* @return 完整内容
*/
public Map<String, Object> generateCompleteContent(String productImageUrl, String productName,
Double originalPrice, Double finalPrice, String style) {
Map<String, Object> result = new HashMap<>();
try {
// 1. 提取关键词
Map<String, Object> keywordResult = extractKeywords(productName);
List<String> keywords = (List<String>) keywordResult.get("keywords");
String keywordsText = (String) keywordResult.get("keywordsText");
// 2. 生成文案
Map<String, Object> contentResult = generateContent(
productName, originalPrice, finalPrice, keywordsText, style
);
String content = (String) contentResult.get("content");
// 3. 生成营销图片
String imageBase64 = null;
if (StrUtil.isNotBlank(productImageUrl)) {
try {
// 使用提取的关键词作为商品名称显示
String displayName = keywords != null && !keywords.isEmpty()
? keywords.get(0)
: productName;
imageBase64 = marketingImageService.generateMarketingImage(
productImageUrl, originalPrice, finalPrice, displayName
);
} catch (Exception e) {
log.warn("生成营销图片失败", e);
}
}
result.put("success", true);
result.put("keywords", keywords);
result.put("keywordsText", keywordsText);
result.put("content", content);
result.put("imageBase64", imageBase64);
} catch (Exception e) {
log.error("生成完整内容失败", e);
result.put("success", false);
result.put("error", "生成完整内容失败: " + e.getMessage());
}
return result;
}
/**
* 简单提取关键词(降级方案)
*/
private List<String> simpleExtractKeywords(String productName) {
List<String> keywords = new ArrayList<>();
// 移除常见规格信息
String cleaned = productName
.replaceAll("\\s*XL|L|M|S|XXL\\s*", "")
.replaceAll("\\s*\\d+/\\d+[A-Z]?\\s*", "")
.replaceAll("\\s*【.*?】\\s*", "")
.replaceAll("\\s*\\(.*?\\)\\s*", "");
// 提取前几个词
String[] words = cleaned.split("\\s+");
for (String word : words) {
if (word.length() >= 2 && word.length() <= 6 && keywords.size() < 5) {
keywords.add(word);
}
}
return keywords;
}
/**
* 从 Redis 获取提示词模板,如果没有则返回默认模板
*
* @param templateKey 模板键名keywords, content:xhs, content:douyin, content:both
* @param defaultTemplate 默认模板
* @return 提示词模板
*/
private String getPromptTemplate(String templateKey, String defaultTemplate) {
if (redisTemplate == null) {
log.debug("Redis未配置使用默认模板: {}", templateKey);
return defaultTemplate;
}
try {
String redisKey = REDIS_KEY_PREFIX + templateKey;
String template = redisTemplate.opsForValue().get(redisKey);
if (StrUtil.isNotBlank(template)) {
log.debug("从Redis读取模板: {}", templateKey);
return template;
} else {
log.debug("Redis中未找到模板使用默认模板: {}", templateKey);
return defaultTemplate;
}
} catch (Exception e) {
log.warn("读取Redis模板失败使用默认模板: {}", templateKey, e);
return defaultTemplate;
}
}
/**
* 生成简单文案(降级方案)
*/
private String generateSimpleContent(String productName, Double originalPrice, Double finalPrice) {
StringBuilder content = new StringBuilder();
content.append("🔥 ").append(productName).append("\n\n");
if (originalPrice != null && finalPrice != null && originalPrice > finalPrice) {
content.append("💰 原价:¥").append(String.format("%.0f", originalPrice)).append("\n");
content.append("💸 到手价:¥").append(String.format("%.0f", finalPrice)).append("\n");
double discount = ((originalPrice - finalPrice) / originalPrice) * 100;
content.append("✨ 立省:¥").append(String.format("%.0f", originalPrice - finalPrice))
.append("").append(String.format("%.0f", discount)).append("%\n\n");
}
content.append("💡 超值好物,不容错过!\n");
content.append("🎁 限时优惠,先到先得!");
return content.toString();
}
}

View File

@@ -32,7 +32,7 @@ import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import static cn.van.business.util.JDUtil.*;
@@ -43,8 +43,10 @@ import static cn.van.business.util.JDUtil.*;
@Slf4j
public class JDProductService {
private static final String LPF_APP_KEY_WZ = "98e21c89ae5610240ec3f5f575f86a59";
private static final String LPF_SECRET_KEY_WZ = "3dcb6b23a1104639ac433fd07adb6dfb";
// 自己的98e21c89ae5610240ec3f5f575f86a59
private static final String LPF_APP_KEY_WZ = "34407d6cae6d43eca740370b8e12b01e";
// 自己的3dcb6b23a1104639ac433fd07adb6dfb
private static final String LPF_SECRET_KEY_WZ = "ad4966e1df3348a185fe6b33aa679a69";
private static final String SERVER_URL = "https://api.jd.com/routerjson";
private static final String ACCESS_TOKEN = "";
private static final Logger logger = LoggerFactory.getLogger(JDProductService.class);
@@ -84,17 +86,29 @@ public class JDProductService {
for (String url : urls) {
try {
String format = dateFormat.format(new Date());
GoodsQueryResult productInfo = queryProductInfoByUJDUrl(url);
String originalUrlInText = url;
String normalizedUrl = normalizeJdUrl(originalUrlInText);
if (normalizedUrl == null) {
log.warn("检测到的链接无法识别为合法京东链接,跳过处理: {}", originalUrlInText);
JSONObject errorObj = new JSONObject();
errorObj.put("url", originalUrlInText);
errorObj.put("error", "链接格式不支持或识别失败");
resultArray.add(errorObj);
continue;
}
GoodsQueryResult productInfo = queryProductInfoByUJDUrl(normalizedUrl);
if (productInfo == null || productInfo.getCode() != 200 || productInfo.getData() == null || productInfo.getData().length == 0) {
JSONObject errorObj = new JSONObject();
errorObj.put("url", url);
errorObj.put("url", originalUrlInText);
errorObj.put("error", "链接查询失败");
resultArray.add(errorObj);
continue;
}
JSONObject productObj = new JSONObject();
productObj.put("originalUrl", url);
productObj.put("originalUrl", originalUrlInText);
productObj.put("normalizedUrl", normalizedUrl);
// 商品基本信息
productObj.put("materialUrl", productInfo.getData()[0].getMaterialUrl());
@@ -124,22 +138,24 @@ public class JDProductService {
// 生成转链后的短链
try {
String shortUrl = transfer(url, null);
String shortUrl = transfer(normalizedUrl, null);
String effectiveUrl = normalizedUrl;
if (shortUrl != null && !shortUrl.isEmpty()) {
productObj.put("shortUrl", shortUrl);
productObj.put("transferSuccess", true);
// 将短链替换原始链接,用于后续文案生成
url = shortUrl;
effectiveUrl = shortUrl;
} else {
productObj.put("shortUrl", url); // 如果转链失败,使用链接
productObj.put("shortUrl", normalizedUrl); // 如果转链失败,使用归一化后的链接
productObj.put("transferSuccess", false);
log.warn("转链失败,使用原链接: {}", url);
log.warn("转链失败,使用原链接: {}", normalizedUrl);
}
productObj.put("effectiveUrl", effectiveUrl);
} catch (Exception e) {
log.error("生成转链时发生异常: {}", url, e);
productObj.put("shortUrl", url); // 转链异常时使用原链接
log.error("生成转链时发生异常: {}", normalizedUrl, e);
productObj.put("shortUrl", normalizedUrl); // 转链异常时使用原链接
productObj.put("transferSuccess", false);
productObj.put("transferError", e.getMessage());
productObj.put("effectiveUrl", normalizedUrl);
}
// 文案信息
@@ -182,13 +198,28 @@ public class JDProductService {
wenan4.put("content", "【教你下单】 " + title + cleanSkuName + "\n" + WENAN_FANAN_BX.replaceAll("信息更新日期:", "信息更新日期:" + format));
wenanArray.add(wenan4);
JSONObject wenan5 = new JSONObject();
wenan5.put("type", "羽绒服专属-标价到手"); // type明确产品类型+下单方式,与其他方案区分
wenan5.put("content", "(羽绒服专属) " + title + cleanSkuName + "\n" + WENAN_YURONGFU.replaceAll("更新", format + "更新"));
wenanArray.add(wenan5);
productObj.put("wenan", wenanArray);
// 添加通用文案 - 使用转链后的短链替换原始链接
JSONObject commonWenan = new JSONObject();
commonWenan.put("type", "通用文案");
// 将原始消息中的链接替换为转链后的短链
String messageWithShortUrl = message.replace(productObj.getString("originalUrl"), url);
String targetUrl = productObj.getString("effectiveUrl");
String normalizedForReplace = productObj.getString("normalizedUrl");
String messageWithShortUrl = message;
if (targetUrl != null) {
String replaced = message.replace(originalUrlInText, targetUrl);
if (replaced.equals(message) && normalizedForReplace != null) {
replaced = replaced.replace(normalizedForReplace, targetUrl);
}
messageWithShortUrl = replaced;
}
commonWenan.put("content", format + FANAN_COMMON + messageWithShortUrl);
wenanArray.add(commonWenan);
@@ -451,4 +482,168 @@ public class JDProductService {
return errorMap;
}
}
/**
* 批量创建礼金券并生成包含礼金的推广链接
*
* @param skuId 商品SKU ID或materialUrl
* @param amount 礼金金额(单位:元)
* @param quantity 每个礼金券的数量
* @param batchSize 批量创建的个数默认20
* @param owner 商品类型g=自营pop=POP
* @param skuName 商品名称
* @return 批量创建结果列表包含giftCouponKey和shortURL
*/
public List<Map<String, Object>> batchCreateGiftCouponsWithLinks(String skuId, double amount, int quantity, int batchSize, String owner, String skuName) {
log.info("开始批量创建礼金券 - SKU={}, 金额={}元, 数量={}, 批次大小={}, Owner={}", skuId, amount, quantity, batchSize, owner);
List<Map<String, Object>> results = new ArrayList<>();
int successCount = 0;
int failCount = 0;
for (int i = 0; i < batchSize; i++) {
Map<String, Object> result = new HashMap<>();
result.put("index", i + 1);
result.put("success", false);
try {
// 创建礼金券
String giftCouponKey = createGiftCoupon(skuId, amount, quantity, owner, skuName);
if (giftCouponKey == null || giftCouponKey.trim().isEmpty()) {
log.error("批量创建礼金券失败 [{}/{}] - giftCouponKey为空", i + 1, batchSize);
result.put("error", "礼金创建失败giftCouponKey为空");
result.put("giftCouponKey", null);
result.put("shortURL", null);
failCount++;
} else {
log.info("批量创建礼金券成功 [{}/{}] - giftCouponKey={}", i + 1, batchSize, giftCouponKey);
// 保存到Redis
try {
saveGiftCouponToRedis(skuId, giftCouponKey, skuName, owner);
} catch (Exception e) {
log.warn("保存礼金到Redis失败但礼金创建成功 - giftCouponKey={}, error={}", giftCouponKey, e.getMessage());
}
// 生成包含礼金的推广链接
String shortURL = null;
try {
// 使用materialUrl或skuId作为原始链接
String originalUrl = skuId;
// transfer方法支持SKU ID或materialUrl直接传入
shortURL = transfer(originalUrl, giftCouponKey);
if (shortURL == null || shortURL.trim().isEmpty()) {
log.warn("生成推广链接失败 - giftCouponKey={}, 礼金创建成功但转链失败", giftCouponKey);
} else {
log.info("生成推广链接成功 [{}/{}] - giftCouponKey={}, shortURL={}", i + 1, batchSize, giftCouponKey, shortURL);
}
} catch (Exception e) {
log.error("生成推广链接异常 - giftCouponKey={}, error={}", giftCouponKey, e.getMessage(), e);
}
result.put("success", true);
result.put("giftCouponKey", giftCouponKey);
result.put("shortURL", shortURL);
successCount++;
}
} catch (Exception e) {
log.error("批量创建礼金券异常 [{}/{}] - error={}", i + 1, batchSize, e.getMessage(), e);
result.put("error", "创建异常: " + e.getMessage());
result.put("giftCouponKey", null);
result.put("shortURL", null);
failCount++;
}
results.add(result);
// 避免请求过快,每创建一张礼金后稍作延迟
if (i < batchSize - 1) {
try {
Thread.sleep(100); // 延迟100ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("批量创建礼金券延迟被中断");
}
}
}
log.info("批量创建礼金券完成 - 总数={}, 成功={}, 失败={}", batchSize, successCount, failCount);
return results;
}
private static final Pattern UJD_LINK_PATTERN = Pattern.compile("^https?://u\\.jd\\.com/[A-Za-z0-9]+[A-Za-z0-9_-]*$", Pattern.CASE_INSENSITIVE);
private static final Pattern JINGFEN_LINK_PATTERN = Pattern.compile("^https?://jingfen\\.jd\\.com/detail/[A-Za-z0-9]+\\.html$", Pattern.CASE_INSENSITIVE);
private static final Pattern TRAILING_SYMBOLS_PATTERN = Pattern.compile("[))】>》。,;;!!??“”\"'、…—\\s]+$");
private static String normalizeJdUrl(String rawUrl) {
if (rawUrl == null || rawUrl.trim().isEmpty()) {
return null;
}
String trimmed = rawUrl.trim();
// 截断常见中文/英文括号后的内容
int cutoffIndex = findCutoffIndex(trimmed);
if (cutoffIndex > -1) {
trimmed = trimmed.substring(0, cutoffIndex);
}
// 去掉末尾的标点符号
trimmed = TRAILING_SYMBOLS_PATTERN.matcher(trimmed).replaceAll("");
if (trimmed.isEmpty()) {
return null;
}
if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) {
trimmed = "https://" + trimmed;
}
if (UJD_LINK_PATTERN.matcher(trimmed).matches() || JINGFEN_LINK_PATTERN.matcher(trimmed).matches()) {
return trimmed;
}
// 针对 u.jd.com 链接,尝试进一步截断到第一个不合法字符
if (trimmed.contains("u.jd.com/")) {
int schemeEnd = trimmed.indexOf("u.jd.com/") + "u.jd.com/".length();
StringBuilder sb = new StringBuilder(trimmed.substring(0, schemeEnd));
for (int i = schemeEnd; i < trimmed.length(); i++) {
char c = trimmed.charAt(i);
if (isAllowedShortLinkChar(c)) {
sb.append(c);
} else {
break;
}
}
String candidate = sb.toString();
if (UJD_LINK_PATTERN.matcher(candidate).matches()) {
return candidate;
}
}
return null;
}
private static int findCutoffIndex(String text) {
char[] stopChars = new char[]{'', '(', '[', '【', '<', '《', '「', '『'};
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (Character.isWhitespace(c)) {
return i;
}
for (char stopChar : stopChars) {
if (c == stopChar) {
return i;
}
}
}
return -1;
}
private static boolean isAllowedShortLinkChar(char c) {
return (c >= 'A' && c <= 'Z')
|| (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9')
|| c == '-' || c == '_' || c == '.';
}
}

View File

@@ -78,8 +78,10 @@ public class JDUtil {
*/
// van论坛
private static final String LPF_APP_KEY_WZ = "98e21c89ae5610240ec3f5f575f86a59";
private static final String LPF_SECRET_KEY_WZ = "3dcb6b23a1104639ac433fd07adb6dfb";
// 自己的98e21c89ae5610240ec3f5f575f86a59
private static final String LPF_APP_KEY_WZ = "34407d6cae6d43eca740370b8e12b01e";
// 自己的3dcb6b23a1104639ac433fd07adb6dfb
private static final String LPF_SECRET_KEY_WZ = "ad4966e1df3348a185fe6b33aa679a69";
private static final String SERVER_URL = "https://api.jd.com/routerjson";
//accessToken
@@ -107,8 +109,24 @@ public class JDUtil {
5:全国联保,全国统一安装标准。支持官方 400服务号查询假一赔十。
""";
public static final String WENAN_FANAN_BX = "本人提供免费指导下单服务,一台也是团购价,细心指导\n" + "\n" + "【质量】官旗下单,包正的\n" + "【物流】您自己账户可跟踪24小时发货\n" + "【售后】您自己账户直接联系,无忧售后\n" + "【安装】专业人员安装,全程无需您操心\n" + "【价格】标价就是到手价,骑共享单车去酒吧,该省省该花花\n" + "【服务】手把手教您下单,有问题随时咨询\n" + "【体验】所有服务都是官旗提供,价格有内部渠道优惠,同品质更优惠!\n" + "\n" + "信息更新日期:\n" + "\n" + "捡漏价格不定时有变动,优惠不等人,发「省份+型号」免费咨询当日最低价!";
public static final String WENAN_YURONGFU = "坦博尔正品羽绒服!大额优惠券直接送!立省几百\n" +
"品牌官方授权渠道直发,比旗舰自营店到手价更低,用你自己的账号下单更放心~\n" +
"款色码全任你选(仅限店内展示款式),下单前必询库存!拒绝盲拍哦~\n" +
"先确认货号、颜色、尺码,再拍不踩雷,建议先去实体店试穿合身,避免后续麻烦呀\n" +
"到手价 = 页面标价 + 6 元代拍费(划重点:不是所有款都二百多,以页面标价为准)\n" +
"叠加专属优惠券后,比自己直接买省不少,福利不等人!\n" +
"正品保障拉满!假一罚十,支持任何渠道验货无忧\n" +
"后续有质量问题,直接用自己的账号走官方售后,售后有保障\n" +
"目前仅店内展示款式可拍,暂时没有额外款式补充哈\n" +
"粉丝优先回复处理,官方渠道直发,代拍不退代拍费,望理解~\n" +
"\n" +
"更新\n" + // 日期替换标记代码自动替换为“yyyyMMdd更新”
"\n" ;
public static final String FANAN_COMMON = "\n 文案复制到微x点击领券把商品加到J东去APP结算才能显示折扣补贴\n";
/**
* 内部单号:
* 分销标记(标记用,勿改):
@@ -2030,7 +2048,7 @@ public class JDUtil {
/**
* 提供给对外API使用根据商品类型选择一条评论
* 优先返回未使用的京东评论;若无则尝试淘宝映射;仍无则从已使用中随机一条。
* 优先返回未使用的京东评论;若无则尝试淘宝映射;仍无则从已使用中随机一条(京东和淘宝随机选择)
* 返回的 Comment 可能不是持久化实体(如淘宝生成),调用方需自行处理 images 解析。
*/
public synchronized cn.van.business.model.pl.Comment selectCommentForProductType(String productType) {
@@ -2061,18 +2079,55 @@ public class JDUtil {
return chosen;
}
// 无可用京东评论时,尝试淘宝映射生成
// 无可用京东评论时,尝试淘宝映射生成(未使用的淘宝评论)
cn.van.business.model.pl.Comment taobao = null;
try {
cn.van.business.model.pl.Comment taobao = generateTaobaoComment(productType);
taobao = generateTaobaoComment(productType);
if (taobao != null) {
return taobao; // 注意:此处返回的对象未必持久化
}
} catch (Exception ignore) {}
// 兜底:从已使用京东评论里取一条
// 兜底:当京东和淘宝的未使用评论都用完了,在京东已使用和淘宝已使用之间随机选择
java.util.List<cn.van.business.model.pl.Comment> allUsedComments = new java.util.ArrayList<>();
// 添加已使用的京东评论
if (usedComments != null && !usedComments.isEmpty()) {
java.util.Collections.shuffle(usedComments);
return usedComments.get(0);
allUsedComments.addAll(usedComments);
}
// 添加已使用的淘宝评论
try {
getProductTypeMapForTB();
String taobaoProductId = productTypeMapTB.getOrDefault(productId, productId);
java.util.List<TaobaoComment> usedTbComments = taobaoCommentRepository
.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(taobaoProductId, 0);
if (usedTbComments != null && !usedTbComments.isEmpty()) {
// 将淘宝评论转换为京东评论格式
for (TaobaoComment tbComment : usedTbComments) {
cn.van.business.model.pl.Comment converted = new cn.van.business.model.pl.Comment();
converted.setCommentText(tbComment.getCommentText());
String pictureUrls = tbComment.getPictureUrls();
if (pictureUrls != null) {
pictureUrls = pictureUrls.replace("//img.", "https://img.");
}
converted.setPictureUrls(pictureUrls);
converted.setCommentId(tbComment.getCommentId());
converted.setProductId(productId);
converted.setUserName(tbComment.getUserName());
converted.setCreatedAt(tbComment.getCreatedAt());
allUsedComments.add(converted);
}
}
} catch (Exception e) {
logger.warn("获取已使用的淘宝评论失败", e);
}
// 从所有已使用的评论(京东+淘宝)中随机选择一条
if (!allUsedComments.isEmpty()) {
java.util.Collections.shuffle(allUsedComments);
return allUsedComments.get(0);
}
} catch (Exception e) {
logger.error("selectCommentForProductType error", e);

View File

@@ -68,9 +68,13 @@ public class OrderUtil {
if (!isAutoFlush || !lastValidCode.equals(newValidCode)) {
String content = getFormattedOrderInfo(orderRow);
String wxId = getWxidFromJdid(orderRow.getUnionId().toString());
// 根据unionId获取接收人列表
String unionIdStr = orderRow.getUnionId().toString();
String touser = WXUtil.getTouserByUnionId(unionIdStr);
logger.info("京粉订单推送 - unionId={}, wxId={}, touser={}", unionIdStr, wxId, touser);
if (Util.isNotEmpty(wxId)) {
wxUtil.sendTextMessage(wxId, content, 1, wxId, true);
wxUtil.sendTextMessage(wxId, content, 1, wxId, true, touser);
// 不是已完成,不是违规的才发送
if (newValidCode != 17 && newValidCode != 25 && newValidCode != 26 && newValidCode != 27 && newValidCode != 28) {
// 发送今日统计信息
@@ -98,13 +102,15 @@ public class OrderUtil {
}
if (shouldNotify) {
String wxId = getWxidFromJdid(orderRow.getUnionId().toString());
// 根据unionId获取接收人列表
String touser = WXUtil.getTouserByUnionId(orderRow.getUnionId().toString());
if (Util.isNotEmpty(wxId)) {
String content = getFormattedOrderInfoForJB(orderRow);
String alertMsg = "[爱心] 价保/赔付 " + newProPriceAmount + " [爱心] \n" + content;
try {
// 先发送通知
wxUtil.sendTextMessage(wxId, alertMsg, 1, wxId, true);
wxUtil.sendTextMessage(wxId, alertMsg, 1, wxId, true, touser);
// 通知成功后更新Redis格式为 "金额:true"
if (!isAutoFlushJB) {
@@ -176,6 +182,8 @@ public class OrderUtil {
if (!orderRowList.isEmpty()) {
int i = 1;
String wxId = getWxidFromJdid(orderRowList.get(0).getUnionId().toString());
// 根据unionId获取接收人列表
String touser = WXUtil.getTouserByUnionId(orderRowList.get(0).getUnionId().toString());
StringBuilder content = new StringBuilder();
content.append("批量订单:\n\r ").append("").append(orderRowList.size()).append("\r");
List<OrderRow> filterList = orderRowList.stream().filter(orderRow -> orderRow.getValidCode() != 2 && orderRow.getValidCode() != 3).toList();
@@ -187,7 +195,7 @@ public class OrderUtil {
}
if (Util.isNotEmpty(wxId)) {
wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false);
wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false, touser);
}
}
@@ -247,6 +255,8 @@ public class OrderUtil {
if (!orderRowList.isEmpty()) {
int i = 1;
String wxId = getWxidFromJdid(orderRowList.get(0).getUnionId().toString());
// 根据unionId获取接收人列表
String touser = WXUtil.getTouserByUnionId(orderRowList.get(0).getUnionId().toString());
StringBuilder content = new StringBuilder();
content.append("批量订单:\n\r ").append("").append(orderRowList.size()).append("\r");
List<OrderRow> filterList = orderRowList.stream().filter(orderRow -> orderRow.getValidCode() != 2 && orderRow.getValidCode() != 3).toList();
@@ -258,7 +268,7 @@ public class OrderUtil {
}
if (Util.isNotEmpty(wxId)) {
wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false);
wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false, touser);
}
}

View File

@@ -133,6 +133,33 @@ public class WXUtil {
return result;
}
/**
* 根据unionId获取SuperAdmin的接收人列表
* @param unionId 联盟ID
* @return 接收人列表企业微信用户ID多个用逗号分隔如果未配置则返回null
*/
public static String getTouserByUnionId(String unionId) {
if (unionId == null || unionId.trim().isEmpty()) {
logger.debug("getTouserByUnionId: unionId为空");
return null;
}
logger.debug("getTouserByUnionId: 查找unionId={}, super_admins数量={}", unionId, super_admins.size());
for (SuperAdmin admin : super_admins.values()) {
if (unionId.equals(admin.getUnionId())) {
String touser = admin.getTouser();
logger.debug("getTouserByUnionId: 找到匹配的SuperAdmin, unionId={}, name={}, touser={}",
admin.getUnionId(), admin.getName(), touser);
if (touser != null && !touser.trim().isEmpty()) {
return touser.trim();
} else {
logger.debug("getTouserByUnionId: SuperAdmin的touser字段为空或未配置");
}
}
}
logger.debug("getTouserByUnionId: 未找到匹配的SuperAdmin, unionId={}", unionId);
return null;
}
public static List<String> splitStringByLength(String input, int length) {
List<String> result = new ArrayList<>();
@@ -158,7 +185,8 @@ public class WXUtil {
jdidToWxidMap.put(superAdmin.getUnionId(), superAdmin.getWxid());
jdidToRemarkMap.put(superAdmin.getUnionId(), superAdmin.getName());
}
logger.info("超级管理员:{} {}", superAdmin.getName(), superAdmin.getWxid());
logger.info("超级管理员:{} {}, unionId={}, touser={}",
superAdmin.getName(), superAdmin.getWxid(), superAdmin.getUnionId(), superAdmin.getTouser());
}
/* 内部管理群 */
@@ -218,6 +246,10 @@ public class WXUtil {
}
public void sendTextMessage(String wxid, String content, Integer msgType, String fromwxid, Boolean hiddenTime) {
sendTextMessage(wxid, content, msgType, fromwxid, hiddenTime, null);
}
public void sendTextMessage(String wxid, String content, Integer msgType, String fromwxid, Boolean hiddenTime, String touser) {
// 全部打印
//logger.info("发送文本消息 msgType: {} wxid: {} fromwxid: {} content: {}", msgType, wxid, fromwxid, content);
// 先在content顶部插入时间戳
@@ -264,6 +296,10 @@ public class WXUtil {
data.put("msgType", msgType);
data.put("fromWxid", fromwxid);
data.put("hiddenTime", hiddenTime);
// 如果提供了接收人列表,添加到数据中
if (touser != null && !touser.trim().isEmpty()) {
data.put("touser", touser.trim());
}
wxReqDate.setData(data);
// wxReqDate 转成 JSONObject
JSONObject message = JSON.parseObject(JSON.toJSONString(wxReqDate));
@@ -355,7 +391,7 @@ public class WXUtil {
return wxReqDate;
}
@Scheduled(cron = "0 * * * * ?")
//@Scheduled(cron = "0 * * * * ?")
public void checkWxStatus() {
WxReqDate wxReqDate = createWxReqData(WXReqType.GET_WX_STATUS);
JSONObject data = new JSONObject();

View File

@@ -59,11 +59,29 @@ public class WxtsUtil {
* @param hiddenTime 是否隐藏时间戳
*/
public void sendWxTextMessage(String wxid, String content, Integer msgType, String fromWxid, Boolean hiddenTime) {
sendWxTextMessage(wxid, content, msgType, fromWxid, hiddenTime, null);
}
/**
* 发送微信文本消息到wxts接口带接收人参数
* @param wxid 接收者微信ID
* @param content 消息内容
* @param msgType 消息类型
* @param fromWxid 发送者微信ID
* @param hiddenTime 是否隐藏时间戳
* @param touser 接收人列表企业微信用户ID多个用逗号分隔
*/
public void sendWxTextMessage(String wxid, String content, Integer msgType, String fromWxid, Boolean hiddenTime, String touser) {
try {
String url = SERVER_URL + "/wx/send/jd";
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("text", content);
// 如果提供了接收人列表,添加到参数中
if (touser != null && !touser.trim().isEmpty()) {
paramMap.put("touser", touser.trim());
logger.info("企业微信推送设置接收人 - 接收人: {}", touser);
}
HttpResponse execute = HttpRequest.post(url)
.header("vanToken", TOKEN)
@@ -72,7 +90,7 @@ public class WxtsUtil {
.execute();
if (execute.getStatus() == 200) {
logger.info("微信文本消息发送成功wxid={}, content={}", wxid, content);
logger.info("微信文本消息发送成功wxid={}, content={}, touser={}", wxid, content, touser);
} else {
logger.error("微信文本消息发送失败status={}, response={}", execute.getStatus(), execute.body());
}