21 Angajamente cf3e885559 ... 86fc4bc847

Autor SHA1 Permisiunea de a trimite mesaje. Dacă este dezactivată, utilizatorul nu va putea trimite nici un fel de mesaj Data
  tanlie 86fc4bc847 hi 1 săptămână în urmă
  tanlie fabd9a5927 structure simplify. 1 săptămână în urmă
  tanlie 3997bcb677 去掉异步执行。 1 săptămână în urmă
  tanlie a77345a7e3 feat: 管理员地图点击创建许愿树功能 2 săptămâni în urmă
  tanlie fb34557162 auto add user 2 săptămâni în urmă
  tanlie 08ed3e0bb0 refactor: 异步生成许愿图片逻辑提取至 ImageService 2 săptămâni în urmă
  tanlie 1eaf700118 feat: MobileMvcConfig 新增线程池配置、许愿接口增加 type 参数 2 săptămâni în urmă
  tanlie ec16d51ffe feat: 聊天历史接口适配分页 + 支持 image 类型消息展示 2 săptămâni în urmă
  tanlie ade8288036 fix: ImageServiceImpl 下载 Qwen AI 图片到本地目录 2 săptămâni în urmă
  tanlie d85f6be2e8 plan 2 săptămâni în urmă
  tanlie 0dbe3202a2 fix: 智能客服输入框改为 textarea 支持多行输入 2 săptămâni în urmă
  tanlie 84bc2784c2 /** 会话 ID,用于关联上下文记忆 */ 2 săptămâni în urmă
  tanlie 391c93eb80 fix: 修复移动端相机拍摄第二张图片无法上传 2 săptămâni în urmă
  tanlie 2a6fa8704a feat: 集成 JDBC Chat Memory 实现 AI 对话上下文记忆 2 săptămâni în urmă
  tanlie fb91dac86f 集成时间和天气预告。 2 săptămâni în urmă
  tanlie 850a170933 时间tools 2 săptămâni în urmă
  tanlie ac592be92a fix: 修复 AI 许愿建议卡宽度及距离过远时直接拒绝 2 săptămâni în urmă
  tanlie 82155a38c2 feat: AI 助手一键许愿功能 2 săptămâni în urmă
  tanlie 8e323f5bbd feat: request.ts 增加 401 未授权处理,提示并跳转登录页 2 săptămâni în urmă
  tanlie c3f429c96e 配置deepseek key 2 săptămâni în urmă
  tanlie 3186b8fd93 feat: 许愿树详情页加入 AI 智能聊天客服 2 săptămâni în urmă
93 a modificat fișierele cu 2483 adăugiri și 246 ștergeri
  1. 1 0
      .gitignore
  2. BIN
      chinese_ink_cat.png
  3. 0 105
      wishing-platform/platform-base/pom.xml
  4. 2 2
      wishing-platform/platform-core/platform-core-base/pom.xml
  5. 2 7
      wishing-platform/platform-core/platform-core-base/src/main/java/cn/qinys/platform/base/config/BaseBeanConfig.java
  6. 20 42
      wishing-platform/platform-core/platform-core-base/src/main/java/cn/qinys/platform/base/redis/RedisConfig.java
  7. 2 2
      wishing-platform/platform-core/platform-core-base/src/main/java/cn/qinys/platform/base/redis/RedisConfigCacheManager.java
  8. 18 1
      wishing-platform/platform-core/platform-core-base/src/main/java/cn/qinys/platform/base/security/utils/CurrentUtils.java
  9. 1 1
      wishing-platform/platform-core/platform-core-base/src/main/java/cn/qinys/platform/base/utils/PasswordUtils.java
  10. 109 9
      wishing-platform/platform-core/pom.xml
  11. 1 8
      wishing-platform/platform-entity/platform-entity-upms/pom.xml
  12. 1 6
      wishing-platform/platform-entity/platform-entity-wishing/pom.xml
  13. 30 0
      wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/ChatMessage.java
  14. 22 0
      wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/SpringAiChatMemory.java
  15. 2 0
      wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/UserWish.java
  16. 3 0
      wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/WishingTreeExtension.java
  17. 1 0
      wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/WishingUser.java
  18. BIN
      wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/BaseEntity.class
  19. BIN
      wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/WishingTreeExtension.class
  20. 0 3
      wishing-platform/platform-entity/platform-entity-wishing/target/maven-archiver/pom.properties
  21. 1 1
      wishing-platform/platform-entity/pom.xml
  22. 1 1
      wishing-platform/platform-gateway/pom.xml
  23. 1 1
      wishing-platform/platform-gateway/src/main/java/cn/qinys/platform/gateway/config/GlobalExceptionConfiguration.java
  24. 1 1
      wishing-platform/platform-gateway/src/main/java/cn/qinys/platform/gateway/limiter/DiyRequestRateLimiterGatewayFilterFactory.java
  25. 1 1
      wishing-platform/platform-service/platform-service-admin/pom.xml
  26. 16 0
      wishing-platform/platform-service/platform-service-admin/src/main/java/cn/qinys/platform/admin/mapper/WishingTreeExtensionMapper.java
  27. 15 0
      wishing-platform/platform-service/platform-service-admin/src/main/java/cn/qinys/platform/admin/service/GaoDeService.java
  28. 46 0
      wishing-platform/platform-service/platform-service-admin/src/main/java/cn/qinys/platform/admin/service/impl/GaoDeServiceImpl.java
  29. 117 0
      wishing-platform/platform-service/platform-service-admin/src/main/resources/logback-spring.xml
  30. 63 0
      wishing-platform/platform-service/platform-service-admin/src/test/java/cn/qinys/platform/admin/service/GaoDeServiceTest.java
  31. 16 2
      wishing-platform/platform-service/platform-service-mobile/pom.xml
  32. 2 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/MobileApplication.java
  33. 58 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/ChatClientConfig.java
  34. 60 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/FeignConfiguration.java
  35. 25 3
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/MobileMvcConfig.java
  36. 23 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/constants/MsgTypeEnum.java
  37. 23 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/constants/WishtypeEnum.java
  38. 46 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/AdminController.java
  39. 40 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/ChatController.java
  40. 1 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/TreeController.java
  41. 18 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/mapper/ChatMessageMapper.java
  42. 14 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/mapper/SpringAiChatMemoryMapper.java
  43. 23 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/ChatHistoryReq.java
  44. 20 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/ChatReq.java
  45. 2 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/LoginReq.java
  46. 19 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/TreeAddressReq.java
  47. 29 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/TreeCreateReq.java
  48. 2 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/WishCreateReq.java
  49. 18 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/ChatHistoryResp.java
  50. 11 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/ChatResp.java
  51. 2 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/LoginResp.java
  52. 19 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/TreeAddressResp.java
  53. 22 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/ChatService.java
  54. 17 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/ImageService.java
  55. 10 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/TreeService.java
  56. 5 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/AbstractUserWishService.java
  57. 87 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/ChatServiceImpl.java
  58. 115 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/ImageServiceImpl.java
  59. 42 1
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/LoginServiceImpl.java
  60. 60 2
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/TreeServiceImpl.java
  61. 10 1
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/UserWishServiceImpl.java
  62. 24 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/tool/DateTimeTools.java
  63. 45 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/tool/WeatherTools.java
  64. 62 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/util/QwenImage.java
  65. 31 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/openfeign/UpmsFeignClient.java
  66. 7 0
      wishing-platform/platform-service/platform-service-mobile/src/main/resources/application-dev.yml
  67. 7 0
      wishing-platform/platform-service/platform-service-mobile/src/main/resources/application-local.yml
  68. 17 3
      wishing-platform/platform-service/platform-service-mobile/src/main/resources/application.yml
  69. 117 0
      wishing-platform/platform-service/platform-service-mobile/src/main/resources/logback-spring.xml
  70. 28 0
      wishing-platform/platform-service/platform-service-mobile/src/test/java/cn/qinys/platform/mobile/service/impl/UserWishServiceImplTest.java
  71. 23 0
      wishing-platform/platform-service/platform-service-mobile/src/test/java/cn/qinys/platform/mobile/tool/WeatherToolsTest.java
  72. 1 1
      wishing-platform/platform-service/platform-service-upms/pom.xml
  73. 7 1
      wishing-platform/platform-service/platform-service-upms/src/main/java/cn/qinys/platform/upms/api/controller/GaoDeKeyController.java
  74. 6 0
      wishing-platform/platform-service/platform-service-upms/src/main/java/cn/qinys/platform/upms/api/service/IGaoDeKeyService.java
  75. 5 5
      wishing-platform/platform-service/platform-service-upms/src/main/java/cn/qinys/platform/upms/api/service/impl/GaoDeKeyServiceImpl.java
  76. 0 7
      wishing-platform/platform-service/platform-service-upms/src/main/resources/application-dev.yml
  77. 1 7
      wishing-platform/platform-service/platform-service-upms/src/main/resources/application-local.yml
  78. 117 0
      wishing-platform/platform-service/platform-service-upms/src/main/resources/logback-spring.xml
  79. 16 0
      wishing-platform/platform-service/platform-service-upms/src/test/java/cn/qinys/platform/upms/api/service/IGaoDeKeyServiceTest.java
  80. 1 0
      wishing-tree-h5/components.d.ts
  81. 2 1
      wishing-tree-h5/src/api/auth.ts
  82. 23 0
      wishing-tree-h5/src/api/chat.ts
  83. 16 3
      wishing-tree-h5/src/api/request.ts
  84. 16 0
      wishing-tree-h5/src/api/tree.ts
  85. 1 0
      wishing-tree-h5/src/api/wish.ts
  86. 446 0
      wishing-tree-h5/src/components/ChatWidget.vue
  87. 23 15
      wishing-tree-h5/src/components/ImageUploader.vue
  88. 7 2
      wishing-tree-h5/src/stores/user.ts
  89. 1 0
      wishing-tree-h5/src/views/MakeWishView.vue
  90. 114 1
      wishing-tree-h5/src/views/MapView.vue
  91. 3 0
      wishing-tree-h5/src/views/TreeDetailView.vue
  92. BIN
      wishing-tree-h5/wish.zip
  93. BIN
      许愿树创业计划书.pptx

+ 1 - 0
.gitignore

@@ -21,3 +21,4 @@ wishing-platform/platform-entity/.idea/
 wishing-platform/platform-gateway/.idea/
 wishing-platform/platform-service/.idea/
 wishing-platform/platform-base/.idea/
+wishing-platform/platform-entity/platform-entity-wishing/target/

BIN
chinese_ink_cat.png


+ 0 - 105
wishing-platform/platform-base/pom.xml

@@ -1,105 +0,0 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <groupId>cn.qinys</groupId>
-    <artifactId>wishing-platform</artifactId>
-    <packaging>pom</packaging>
-    <version>1.0.0-SNAPSHOT</version>
-    <properties>
-        <maven.compiler.source>21</maven.compiler.source>
-        <maven.compiler.target>21</maven.compiler.target>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <spring-cloud.version>2025.0.2</spring-cloud.version>
-        <spring-boot.version>3.5.14</spring-boot.version>
-        <spring-cloud-alibaba.version>2025.0.0.0</spring-cloud-alibaba.version>
-        <fastjson-version>2.0.32</fastjson-version>
-        <mybatis-plus-version>3.5.15</mybatis-plus-version>
-        <kaptcha.version>2.3.2</kaptcha.version>
-        <hutool-all-version>5.8.32</hutool-all-version>
-        <mysql-connector-version>8.0.33</mysql-connector-version>
-        <alibaba.ttl.version>2.14.2</alibaba.ttl.version>
-        <spring-ai.version>1.1.7</spring-ai.version>
-    </properties>
-    <dependencyManagement>
-        <dependencies>
-            <!--spring cloud 版本管理-->
-            <dependency>
-                <groupId>org.springframework.cloud</groupId>
-                <artifactId>spring-cloud-dependencies</artifactId>
-                <version>${spring-cloud.version}</version>
-                <type>pom</type>
-                <scope>import</scope>
-            </dependency>
-            <!--spring boot 版本管理-->
-            <dependency>
-                <groupId>org.springframework.boot</groupId>
-                <artifactId>spring-boot-starter-parent</artifactId>
-                <version>${spring-boot.version}</version>
-                <type>pom</type>
-                <scope>import</scope>
-            </dependency>
-            <!--spring 阿里巴巴 版本管理-->
-            <dependency>
-                <groupId>com.alibaba.cloud</groupId>
-                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
-                <version>${spring-cloud-alibaba.version}</version>
-                <type>pom</type>
-                <scope>import</scope>
-            </dependency>
-            <dependency>
-                <groupId>com.alibaba</groupId>
-                <artifactId>fastjson</artifactId>
-                <version>${fastjson-version}</version>
-            </dependency>
-            <dependency>
-                <groupId>com.baomidou</groupId>
-                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
-                <version>${mybatis-plus-version}</version>
-            </dependency>
-            <dependency>
-                <groupId>com.baomidou</groupId>
-                <artifactId>mybatis-plus-extension</artifactId>
-                <version>${mybatis-plus-version}</version>
-            </dependency>
-            <dependency>
-                <groupId>com.baomidou</groupId>
-                <artifactId>mybatis-plus-jsqlparser</artifactId>
-                <version>${mybatis-plus-version}</version>
-            </dependency>
-            <!--数据库-->
-            <dependency>
-                <groupId>mysql</groupId>
-                <artifactId>mysql-connector-java</artifactId>
-                <version>${mysql-connector-version}</version>
-            </dependency>
-            <dependency>
-                <groupId>com.github.penggle</groupId>
-                <artifactId>kaptcha</artifactId>
-                <version>${kaptcha.version}</version>
-            </dependency>
-            <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
-            <dependency>
-                <groupId>cn.hutool</groupId>
-                <artifactId>hutool-all</artifactId>
-                <version>${hutool-all-version}</version>
-            </dependency>
-            <dependency>
-                <groupId>org.hibernate.validator</groupId>
-                <artifactId>hibernate-validator</artifactId>
-                <version>8.0.1.Final</version>
-            </dependency>
-            <dependency>
-                <groupId>com.alibaba</groupId>
-                <artifactId>transmittable-thread-local</artifactId>
-                <version>${alibaba.ttl.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>org.springframework.ai</groupId>
-                <artifactId>spring-ai-bom</artifactId>
-                <version>${spring-ai.version}</version>
-                <type>pom</type>
-                <scope>import</scope>
-            </dependency>
-        </dependencies>
-    </dependencyManagement>
-</project>

+ 2 - 2
wishing-platform/platform-core/platform-core-base/pom.xml

@@ -3,7 +3,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>cn.qinys</groupId>
-        <artifactId>platform-core</artifactId>
+        <artifactId>wishing-platform</artifactId>
         <version>1.0.0-SNAPSHOT</version>
     </parent>
 
@@ -41,7 +41,7 @@
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-aop</artifactId>
+            <artifactId>spring-boot-starter-aspectj</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 2 - 7
wishing-platform/platform-core/platform-core-base/src/main/java/cn/qinys/platform/base/config/BaseBeanConfig.java

@@ -1,12 +1,9 @@
 package cn.qinys.platform.base.config;
 
-import org.springframework.boot.web.client.RestTemplateBuilder;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.client.RestTemplate;
 
-import java.time.Duration;
-
 /**
  * Description:
  *
@@ -18,10 +15,8 @@ public class BaseBeanConfig {
 
     @Bean
     RestTemplate restTemplate() {
-        RestTemplateBuilder builder = new RestTemplateBuilder();
-        builder.connectTimeout(Duration.ofSeconds(30));
-        builder.readTimeout(Duration.ofSeconds(120));
-        return builder.build();
+        RestTemplate restTemplate = new RestTemplate();
+        return restTemplate;
     }
 
 }

+ 20 - 42
wishing-platform/platform-core/platform-core-base/src/main/java/cn/qinys/platform/base/redis/RedisConfig.java

@@ -1,8 +1,7 @@
 package cn.qinys.platform.base.redis;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
+
+import jakarta.annotation.Resource;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -12,12 +11,9 @@ import org.springframework.data.redis.cache.RedisCacheWriter;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.core.StringRedisTemplate;
-import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
 import org.springframework.data.redis.serializer.RedisSerializationContext;
 import org.springframework.data.redis.serializer.RedisSerializer;
-import org.springframework.data.redis.serializer.StringRedisSerializer;
 
-import java.net.UnknownHostException;
 import java.time.Duration;
 
 /**
@@ -29,61 +25,43 @@ import java.time.Duration;
 @EnableCaching
 @Configuration
 public class RedisConfig {
+
+    @Resource
+    private RedisConnectionFactory factory;
     /**
      * 缓存管理器
-     *
-     * @param factory
-     * @return
      */
     @Bean
-    @SuppressWarnings("all")
-    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
-        ObjectMapper om = new ObjectMapper();
-        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
-        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
-        // 解决查询缓存转换异常的问题
-        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
-        jackson2JsonRedisSerializer.setObjectMapper(om);
-        // 配置序列化(解决乱码的问题)
+    public RedisCacheManager cacheManager() {
         RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                 .entryTtl(Duration.ofMillis(-1))
-                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
-                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
+                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
+                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()))
                 .disableCachingNullValues();
-        RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(factory);
 
+        RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(factory);
         return new RedisConfigCacheManager(cacheWriter, config);
     }
 
-    //重写template配置累
+    /**
+     * RedisTemplate 配置
+     */
     @Bean
-    @SuppressWarnings("all")
-    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) throws UnknownHostException {
-
-        RedisTemplate<String, Object> template = new RedisTemplate();
+    public RedisTemplate<String, Object> redisTemplate() {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
         template.setConnectionFactory(factory);
-        //json序列化
-        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
-        ObjectMapper om = new ObjectMapper();
-        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
-        jackson2JsonRedisSerializer.setObjectMapper(om);
-        //String 序列化
-        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
-        //key 采用string序列化
-        template.setKeySerializer(stringRedisSerializer);
-        template.setHashKeySerializer(stringRedisSerializer);  //hash key也采用string
-        //value 采用json
-        template.setValueSerializer(jackson2JsonRedisSerializer);
-        template.setHashValueSerializer(jackson2JsonRedisSerializer);
+
+        template.setKeySerializer(RedisSerializer.string());
+        template.setHashKeySerializer(RedisSerializer.string());
+        template.setValueSerializer(RedisSerializer.json());
+        template.setHashValueSerializer(RedisSerializer.json());
 
         template.afterPropertiesSet();
         return template;
     }
 
     @Bean
-    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
+    public StringRedisTemplate stringRedisTemplate() {
         return new StringRedisTemplate(factory);
     }
 

+ 2 - 2
wishing-platform/platform-core/platform-core-base/src/main/java/cn/qinys/platform/base/redis/RedisConfigCacheManager.java

@@ -2,8 +2,8 @@ package cn.qinys.platform.base.redis;
 
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.data.redis.cache.*;
-import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
 import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.RedisSerializer;
 
 import java.time.Duration;
 
@@ -22,7 +22,7 @@ public class RedisConfigCacheManager extends RedisCacheManager {
     }
 
     private static final RedisSerializationContext.SerializationPair<Object> DEFAULT_PAIR = RedisSerializationContext.SerializationPair
-            .fromSerializer(new GenericJackson2JsonRedisSerializer());
+            .fromSerializer(RedisSerializer.json());
 
     /**
      * @param name

+ 18 - 1
wishing-platform/platform-core/platform-core-base/src/main/java/cn/qinys/platform/base/security/utils/CurrentUtils.java

@@ -1,7 +1,6 @@
 package cn.qinys.platform.base.security.utils;
 
 
-
 import cn.qinys.platform.base.redis.RedisKeyEnum;
 import cn.qinys.platform.base.security.constants.Constants;
 import cn.qinys.platform.base.utils.RedisUtils;
@@ -35,6 +34,9 @@ public class CurrentUtils {
      */
     public static String getCurrentUserId() {
         String token = getToken();
+        if (token == null) {
+            return "0";
+        }
         RedisUtils redisUtils = SpringUtil.getBean(RedisUtils.class);
         LoginUserInfo userInfo = (LoginUserInfo) redisUtils.get(RedisKeyEnum.CURRENT_USER.getCode() + token);
         return userInfo == null ? "0" : userInfo.getId();
@@ -48,6 +50,9 @@ public class CurrentUtils {
      */
     public static String getCurrentWxId() {
         String token = getToken();
+        if (token == null) {
+            return null;
+        }
         RedisUtils redisUtils = SpringUtil.getBean(RedisUtils.class);
         LoginUserInfo userInfo = (LoginUserInfo) redisUtils.get(RedisKeyEnum.CURRENT_USER.getCode() + token);
         return userInfo == null ? null : userInfo.getWxId();
@@ -60,6 +65,9 @@ public class CurrentUtils {
      */
     public static String getCurrentUsername() {
         String token = getToken();
+        if (token == null) {
+            return null;
+        }
         RedisUtils redisUtils = SpringUtil.getBean(RedisUtils.class);
         LoginUserInfo userInfo = (LoginUserInfo) redisUtils.get(RedisKeyEnum.CURRENT_USER.getCode() + token);
         return userInfo == null ? "" : userInfo.getUsername();
@@ -94,6 +102,9 @@ public class CurrentUtils {
      */
     public static LoginUserInfo getCurrentUserInfo() {
         String token = getToken();
+        if (token == null) {
+            return null;
+        }
         RedisUtils redisUtils = SpringUtil.getBean(RedisUtils.class);
         return (LoginUserInfo) redisUtils.get(RedisKeyEnum.CURRENT_USER.getCode() + token);
     }
@@ -106,6 +117,9 @@ public class CurrentUtils {
      */
     public static List<String> getPermissionCodes() {
         String token = getToken();
+        if (token == null) {
+            return null;
+        }
         RedisUtils redisUtils = SpringUtil.getBean(RedisUtils.class);
         LoginUserInfo userInfo = (LoginUserInfo) redisUtils.get(RedisKeyEnum.CURRENT_USER.getCode() + token);
         return userInfo == null || userInfo.getPermissionCodes() == null ? new ArrayList<>() : userInfo.getPermissionCodes();
@@ -114,6 +128,9 @@ public class CurrentUtils {
 
     public static String getToken() {
         ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes == null) {
+            return null;
+        }
         HttpServletRequest request = attributes.getRequest();
         String token = request.getHeader(Constants.JwtEnum.AUTHORIZATION.getCode());
         return token == null ? null : token.replaceFirst(Constants.JwtEnum.BEARER.getCode(), "").trim();

+ 1 - 1
wishing-platform/platform-core/platform-core-base/src/main/java/cn/qinys/platform/base/utils/PasswordUtils.java

@@ -1,7 +1,7 @@
 package cn.qinys.platform.base.utils;
 
+import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.tomcat.util.codec.binary.Base64;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;

+ 109 - 9
wishing-platform/platform-core/pom.xml

@@ -1,17 +1,117 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
-    <parent>
-        <groupId>cn.qinys</groupId>
-        <artifactId>wishing-platform</artifactId>
-        <version>1.0.0-SNAPSHOT</version>
-    </parent>
-    <artifactId>platform-core</artifactId>
+    <groupId>cn.qinys</groupId>
+    <artifactId>wishing-platform</artifactId>
     <packaging>pom</packaging>
-    <modules>
-        <module>platform-core-base</module>
-    </modules>
+    <version>1.0.0-SNAPSHOT</version>
     <properties>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <spring-cloud.version>2025.1.2</spring-cloud.version>
+        <spring-boot.version>4.1.0</spring-boot.version>
+        <spring-cloud-alibaba.version>2025.1.0.0</spring-cloud-alibaba.version>
+        <fastjson-version>2.0.32</fastjson-version>
+        <mybatis-plus-version>3.5.15</mybatis-plus-version>
+        <kaptcha.version>2.3.2</kaptcha.version>
+        <hutool-all-version>5.8.32</hutool-all-version>
+        <mysql-connector-version>8.0.33</mysql-connector-version>
+        <alibaba.ttl.version>2.14.2</alibaba.ttl.version>
+        <spring-ai.version>2.0.0</spring-ai.version>
     </properties>
+    <dependencyManagement>
+        <dependencies>
+            <!--spring cloud 版本管理-->
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring-cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!--spring boot 版本管理-->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-parent</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!--spring 阿里巴巴 版本管理-->
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
+                <version>${spring-cloud-alibaba.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>fastjson</artifactId>
+                <version>${fastjson-version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
+                <version>${mybatis-plus-version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-extension</artifactId>
+                <version>${mybatis-plus-version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-jsqlparser</artifactId>
+                <version>${mybatis-plus-version}</version>
+            </dependency>
+            <!--数据库-->
+            <dependency>
+                <groupId>mysql</groupId>
+                <artifactId>mysql-connector-java</artifactId>
+                <version>${mysql-connector-version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.github.penggle</groupId>
+                <artifactId>kaptcha</artifactId>
+                <version>${kaptcha.version}</version>
+            </dependency>
+            <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-all</artifactId>
+                <version>${hutool-all-version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.hibernate.validator</groupId>
+                <artifactId>hibernate-validator</artifactId>
+                <version>8.0.1.Final</version>
+            </dependency>
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>transmittable-thread-local</artifactId>
+                <version>${alibaba.ttl.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.ai</groupId>
+                <artifactId>spring-ai-bom</artifactId>
+                <version>${spring-ai.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>com.alibaba.fastjson2</groupId>
+                <artifactId>fastjson2</artifactId>
+                <version>2.0.53</version>
+            </dependency>
+            <!-- Source: https://mvnrepository.com/artifact/tools.jackson.core/jackson-databind -->
+            <dependency>
+                <groupId>tools.jackson.core</groupId>
+                <artifactId>jackson-databind</artifactId>
+                <version>3.2.0</version>
+                <scope>compile</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
 </project>

+ 1 - 8
wishing-platform/platform-entity/platform-entity-upms/pom.xml

@@ -16,13 +16,6 @@
     </properties>
 
     <dependencies>
-        <dependency>
-            <groupId>org.projectlombok</groupId>
-            <artifactId>lombok</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
-        </dependency>
+       
     </dependencies>
 </project>

+ 1 - 6
wishing-platform/platform-entity/platform-entity-wishing/pom.xml

@@ -18,11 +18,6 @@
     </properties>
 
     <dependencies>
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>3.8.1</version>
-            <scope>test</scope>
-        </dependency>
+
     </dependencies>
 </project>

+ 30 - 0
wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/ChatMessage.java

@@ -0,0 +1,30 @@
+package cn.qinys.platform.entity.wishing;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("chat_message")
+public class ChatMessage extends BaseEntity {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 用户标识 */
+    private String userId;
+
+    /** 关联许愿树 ID */
+    private Integer treeId;
+
+    /** 角色:user / ai */
+    private String role;
+
+    /** 消息内容 */
+    private String content;
+
+    private String type;
+}

+ 22 - 0
wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/SpringAiChatMemory.java

@@ -0,0 +1,22 @@
+package cn.qinys.platform.entity.wishing;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-14 11:44
+ **/
+@Data
+@TableName("spring_ai_chat_memory")
+public class SpringAiChatMemory implements Serializable {
+    private String conversationId;
+    private String content;
+    private String type;
+    private LocalDateTime timestamp;
+
+}

+ 2 - 0
wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/UserWish.java

@@ -52,4 +52,6 @@ public class UserWish extends BaseEntity {
      * 0=active, 1=fulfilled
      */
     private Integer status = 0;
+
+    private Integer type;
 }

+ 3 - 0
wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/WishingTreeExtension.java

@@ -35,5 +35,8 @@ public class WishingTreeExtension implements Serializable {
     @TableField(fill = FieldFill.INSERT_UPDATE)
     private LocalDateTime updatedAt;
 
+    private Integer adcode;
+
+    private String regeocode;
 
 }

+ 1 - 0
wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/WishingUser.java

@@ -39,4 +39,5 @@ public class WishingUser extends BaseEntity implements Serializable {
 
     private Integer loginCount;
 
+    private Integer isAdmin;
 }

BIN
wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/BaseEntity.class


BIN
wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/WishingTreeExtension.class


+ 0 - 3
wishing-platform/platform-entity/platform-entity-wishing/target/maven-archiver/pom.properties

@@ -1,3 +0,0 @@
-artifactId=platform-entity-wishing
-groupId=cn.qinys
-version=1.0.0-SNAPSHOT

+ 1 - 1
wishing-platform/platform-entity/pom.xml

@@ -27,7 +27,7 @@
         </dependency>
         <dependency>
             <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
         </dependency>
     </dependencies>
 

+ 1 - 1
wishing-platform/platform-gateway/pom.xml

@@ -46,7 +46,7 @@
         </dependency>
         <dependency>
             <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 1 - 1
wishing-platform/platform-gateway/src/main/java/cn/qinys/platform/gateway/config/GlobalExceptionConfiguration.java

@@ -5,7 +5,7 @@ import cn.qinys.platform.gateway.response.Result;
 import lombok.extern.slf4j.Slf4j;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
+import org.springframework.boot.webflux.error.ErrorWebExceptionHandler;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.Ordered;
 import org.springframework.http.MediaType;

+ 1 - 1
wishing-platform/platform-gateway/src/main/java/cn/qinys/platform/gateway/limiter/DiyRequestRateLimiterGatewayFilterFactory.java

@@ -52,7 +52,7 @@ public class DiyRequestRateLimiterGatewayFilterFactory extends RequestRateLimite
                 ServerHttpResponse httpResponse = exchange.getResponse();
                 //修改code为500
                 httpResponse.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
-                if (!httpResponse.getHeaders().containsKey("Content-Type")) {
+                if (!httpResponse.getHeaders().containsHeader("Content-Type")) {
                     httpResponse.getHeaders().add("Content-Type", "application/json");
                 }
                 //此处无法触发全局异常处理,手动返回

+ 1 - 1
wishing-platform/platform-service/platform-service-admin/pom.xml

@@ -43,7 +43,7 @@
         </dependency>
         <dependency>
             <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
         </dependency>
         <dependency>
             <groupId>com.baomidou</groupId>

+ 16 - 0
wishing-platform/platform-service/platform-service-admin/src/main/java/cn/qinys/platform/admin/mapper/WishingTreeExtensionMapper.java

@@ -0,0 +1,16 @@
+package cn.qinys.platform.admin.mapper;
+
+import cn.qinys.platform.entity.wishing.WishingTreeExtension;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-07 14:53
+ **/
+@Mapper
+public interface WishingTreeExtensionMapper extends BaseMapper<WishingTreeExtension> {
+
+
+}

+ 15 - 0
wishing-platform/platform-service/platform-service-admin/src/main/java/cn/qinys/platform/admin/service/GaoDeService.java

@@ -0,0 +1,15 @@
+package cn.qinys.platform.admin.service;
+
+import com.alibaba.fastjson2.JSONObject;
+
+import java.math.BigDecimal;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-13 11:14
+ **/
+public interface GaoDeService {
+
+    JSONObject getGaoDeResult(BigDecimal lon, BigDecimal lat, Integer index);
+}

+ 46 - 0
wishing-platform/platform-service/platform-service-admin/src/main/java/cn/qinys/platform/admin/service/impl/GaoDeServiceImpl.java

@@ -0,0 +1,46 @@
+package cn.qinys.platform.admin.service.impl;
+
+import cn.qinys.platform.admin.service.GaoDeService;
+import cn.qinys.platform.base.exceptions.BizException;
+import com.alibaba.fastjson2.JSONObject;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.math.BigDecimal;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-13 11:15
+ **/
+@Slf4j
+@Service
+public class GaoDeServiceImpl implements GaoDeService {
+
+    @Resource
+    RestTemplate restTemplate;
+
+    String key = "88a0c82c3f60a748c6c638ef9db16bca";
+
+    @Override
+    public JSONObject getGaoDeResult(BigDecimal lon, BigDecimal lat, Integer index) {
+        if (index >= 100) {
+            return null;
+        }
+        index++;
+        String url = "https://restapi.amap.com/v3/geocode/regeo?key={0}&location={1},{2}";
+        JSONObject res = restTemplate.getForObject(url, JSONObject.class, key, lon, lat);
+        if (res == null) {
+            throw new BizException("逆地理解释出现错误");
+        }
+        if ("1".equals(res.getString("status"))) {
+            log.info("获取逆地理解析信息成功{}", res);
+            return res.getJSONObject("regeocode");
+        } else {
+            return this.getGaoDeResult(lon, lat, index);
+        }
+    }
+
+}

+ 117 - 0
wishing-platform/platform-service/platform-service-admin/src/main/resources/logback-spring.xml

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <contextName>logback</contextName>
+
+    <property name="LOG_HOME" value="./logs/admin"/>
+    <springProperty scope="context" name="applicationName" source="spring.application.name" defaultValue="localhost" />
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>debug</level>
+        </filter>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [${applicationName},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-}] [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <!--2. 输出到文档-->
+    <!-- 2.1 level为 DEBUG 日志,时间滚动输出  -->
+    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 正在记录的日志文档的路径及文档名 -->
+        <file>${LOG_HOME}/debug.log</file>
+        <!--日志文档输出格式-->
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset> <!-- 设置字符集 -->
+        </encoder>
+        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志归档 -->
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <!--日志文档保留天数-->
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <!-- 此日志文档只记录debug级别的 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>debug</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 2.2 level为 INFO 日志,时间滚动输出  -->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/info.log</file>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>info</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 2.3 level为 WARN 日志,时间滚动输出  -->
+    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/warn.log</file>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>warn</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/error.log</file>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <springProfile name="dev">
+        <logger name="com.sdcm.pmp" level="debug"/>
+    </springProfile>
+
+    <root level="info">
+        <appender-ref ref="CONSOLE"/>
+        <appender-ref ref="DEBUG_FILE"/>
+        <appender-ref ref="INFO_FILE"/>
+        <appender-ref ref="WARN_FILE"/>
+        <appender-ref ref="ERROR_FILE"/>
+    </root>
+</configuration>

+ 63 - 0
wishing-platform/platform-service/platform-service-admin/src/test/java/cn/qinys/platform/admin/service/GaoDeServiceTest.java

@@ -0,0 +1,63 @@
+package cn.qinys.platform.admin.service;
+
+import cn.qinys.platform.admin.mapper.WishingTreeExtensionMapper;
+import cn.qinys.platform.admin.mapper.WishingTreeMapper;
+import cn.qinys.platform.entity.wishing.WishingTree;
+import cn.qinys.platform.entity.wishing.WishingTreeExtension;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import jakarta.annotation.Resource;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-13 11:26
+ **/
+
+@SpringBootTest
+class GaoDeServiceTest {
+
+    @Resource
+    private GaoDeService gaoDeService;
+    @Resource
+    WishingTreeMapper treeMapper;
+    @Resource
+    WishingTreeExtensionMapper treeExtensionMapper;
+
+    @Test
+    void getGaoDeResult() {
+        QueryWrapper<WishingTree> queryWrapper = new QueryWrapper<>();
+        queryWrapper.ge("id", 0);
+        List<WishingTree> trees = treeMapper.selectList(queryWrapper);
+        trees.forEach(tree -> {
+            JSONObject gaoDeResult = gaoDeService.getGaoDeResult(tree.getLongitude(), tree.getLatitude(), 5);
+            JSONObject addressComponent = gaoDeResult.getJSONObject("addressComponent");
+            Integer adcode = addressComponent.getInteger("adcode");
+            UpdateWrapper<WishingTreeExtension> updateWrapper = new UpdateWrapper<>();
+            updateWrapper.eq("tree_id", tree.getId());
+            updateWrapper.set("adcode", adcode);
+            updateWrapper.set("regeocode", JSON.toJSONString(gaoDeResult));
+            int update = treeExtensionMapper.update(updateWrapper);
+            if(update == 0){
+                WishingTreeExtension extension = new WishingTreeExtension();
+                extension.setTreeId(tree.getId());
+                extension.setTotalCount(0);
+                extension.setPublicCount(0);
+                extension.setPrivateCount(0);
+                extension.setAdcode(adcode);
+                extension.setRegeocode(JSON.toJSONString(gaoDeResult));
+                treeExtensionMapper.insert(extension);
+            }
+
+        });
+
+    }
+}

+ 16 - 2
wishing-platform/platform-service/platform-service-mobile/pom.xml

@@ -12,7 +12,6 @@
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
-
     <dependencies>
         <dependency>
             <groupId>cn.qinys</groupId>
@@ -40,7 +39,7 @@
         </dependency>
         <dependency>
             <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
         </dependency>
         <dependency>
             <groupId>com.baomidou</groupId>
@@ -58,6 +57,21 @@
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-loadbalancer</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-starter-model-deepseek</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
+        </dependency>
+        <!-- Source: https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>dashscope-sdk-java</artifactId>
+            <version>2.22.22</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
     <build>
         <finalName>wishing-mobile</finalName>

+ 2 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/MobileApplication.java

@@ -2,12 +2,14 @@ package cn.qinys.platform;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
 
 /**
  * @author lie tan
  * @description
  * @date 2026-05-17 21:40
  **/
+@EnableFeignClients
 @SpringBootApplication
 public class MobileApplication {
 

+ 58 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/ChatClientConfig.java

@@ -0,0 +1,58 @@
+package cn.qinys.platform.config;
+
+import jakarta.annotation.Resource;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
+import org.springframework.ai.chat.memory.ChatMemory;
+import org.springframework.ai.chat.memory.MessageWindowChatMemory;
+import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ChatClientConfig {
+
+    @Resource
+    JdbcChatMemoryRepository chatMemoryRepository;
+
+    @Bean
+    MessageChatMemoryAdvisor chatMemoryAdvisor() {
+        ChatMemory chatMemory = MessageWindowChatMemory.builder()
+                .chatMemoryRepository(chatMemoryRepository)
+                .maxMessages(20)
+                .build();
+        return MessageChatMemoryAdvisor.builder(chatMemory).build();
+    }
+
+    @Bean
+    ChatClient chatClient(ChatModel chatModel, MessageChatMemoryAdvisor memoryAdvisor) {
+        return ChatClient.builder(chatModel)
+                .defaultSystem("""
+                        你是许愿树的智能客服小助手,名叫"小愿"。
+                        你的职责是:
+                        1. 为用户介绍许愿树的历史故事、文化传统、许愿方式
+                        2. 解答用户关于许愿的疑问,如如何许愿、许愿礼仪等
+                        3. 为用户推荐附近适合许愿的地点
+                        4. 用温暖、友好的语气与用户交流
+                        5. 适当使用 emoji 让对话更生动
+                        
+                        【重要】一键许愿功能:
+                        当用户表达"帮我许愿""替我许愿""想许愿""许个愿""我要许愿"等意图时,你必须执行以下步骤:
+                        1. 如果用户还没说愿望内容,引导用户说出具体愿望
+                        2. 用户说出愿望后,整理成一句20-50字精炼的愿望文字
+                        3. 从以下类别选2-3个标签:健康、平安、爱情、事业、学业、财富、梦想、好运、祈福、家庭
+                        4. 你的回复末尾必须严格包含以下格式(必须一字不差,这是系统指令):
+                        
+                        [WISH]{"content":"整理后的愿望文字","tags":["标签1","标签2"]}[/WISH]
+                        
+                        示例:用户说"帮我许愿,希望家人健康",你应该回复:
+                        我帮你整理好了愿望,确认一下哦~ [WISH]{"content":"希望家人身体健康,平安喜乐","tags":["健康","平安","家庭"]}[/WISH]
+                        
+                        回复控制在 100-200 字以内,简洁明了。
+                        如果用户问的问题与许愿无关,礼貌地引导回许愿相关话题。
+                        """)
+                .defaultAdvisors(memoryAdvisor)
+                .build();
+    }
+}

+ 60 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/FeignConfiguration.java

@@ -0,0 +1,60 @@
+package cn.qinys.platform.config;
+
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import feign.Retryer;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Feign配置类
+ *
+ * @author v-lishiquan.gx@chinatelecom.cn
+ * @date 2020-08-26
+ */
+@Configuration
+@Slf4j
+public class FeignConfiguration implements RequestInterceptor {
+
+    @Bean
+    Retryer feignRetryer() {
+        return Retryer.NEVER_RETRY;
+    }
+
+    @Override
+    public void apply(RequestTemplate requestTemplate) {
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        if (Objects.nonNull(requestAttributes)) {
+            RequestContextHolder.setRequestAttributes(requestAttributes, true);
+            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
+            HttpServletRequest request = attributes.getRequest();
+            // 添加请求头
+            Enumeration<String> headerNames = request.getHeaderNames();
+            if (headerNames != null) {
+                while (headerNames.hasMoreElements()) {
+                    String name = headerNames.nextElement();
+                    if(this.removeHead().contains(name)){
+                        continue;
+                    }
+                    String values = request.getHeader(name);
+                    requestTemplate.header(name, values);
+                }
+            }
+        }
+    }
+
+    private List<String> removeHead() {
+        return List.of("content-length");
+    }
+}

+ 25 - 3
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/MobileMvcConfig.java

@@ -2,22 +2,44 @@ package cn.qinys.platform.config;
 
 import cn.qinys.platform.properties.SystemProperties;
 import jakarta.annotation.Resource;
-import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.core.task.AsyncTaskExecutor;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@EnableAsync
 @Configuration
 public class MobileMvcConfig implements WebMvcConfigurer {
 
     @Resource
     SystemProperties systemProperties;
 
+    /**
+     * 线程池配置
+     */
+    @Bean("mobileTaskExecutor")
+    public AsyncTaskExecutor mobileTaskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(4);
+        executor.setMaxPoolSize(16);
+        executor.setQueueCapacity(200);
+        executor.setKeepAliveSeconds(60);
+        executor.setThreadNamePrefix("mobile-task-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        executor.setAwaitTerminationSeconds(30);
+        executor.initialize();
+        return executor;
+    }
 
     /**
      * 添加静态资源文件,外部可以直接访问地址
-     *
-     * @param registry
      */
     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry) {

+ 23 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/constants/MsgTypeEnum.java

@@ -0,0 +1,23 @@
+package cn.qinys.platform.mobile.constants;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-14 10:01
+ **/
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public enum MsgTypeEnum {
+
+    TEXT("text", "test message"),
+    IMAGE("image", "image message");
+
+    private String value;
+    private String type;
+}

+ 23 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/constants/WishtypeEnum.java

@@ -0,0 +1,23 @@
+package cn.qinys.platform.mobile.constants;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-14 10:01
+ **/
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public enum WishtypeEnum {
+
+    MANUAL(1, "make by manual"),
+    AUTO(2, "support by ai"),;
+
+    private Integer value;
+    private String description;
+}

+ 46 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/AdminController.java

@@ -0,0 +1,46 @@
+package cn.qinys.platform.mobile.controller;
+
+import cn.qinys.platform.base.response.Result;
+import cn.qinys.platform.mobile.req.TreeAddressReq;
+import cn.qinys.platform.mobile.req.TreeCreateReq;
+import cn.qinys.platform.mobile.resp.TreeAddressResp;
+import cn.qinys.platform.mobile.service.TreeService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-15 19:26
+ **/
+
+@RestController
+@RequestMapping("/dgapi/mobile/admin")
+public class AdminController {
+
+    @Resource
+    private TreeService treeService;
+
+    /**
+     * 创建许愿树(管理员功能)
+     */
+    @PostMapping("/create/tree")
+    public Result<Boolean> create(@RequestBody @Valid TreeCreateReq req) {
+        treeService.create(req);
+        return new Result<>(true);
+    }
+
+    /**
+     * 创建许愿树(管理员功能)
+     */
+    @PostMapping("/get/address")
+    public Result<TreeAddressResp> getAddress(@RequestBody @Valid TreeAddressReq req) {
+        TreeAddressResp address = treeService.getAddress(req);
+        return new Result<>(address);
+    }
+
+}

+ 40 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/ChatController.java

@@ -0,0 +1,40 @@
+package cn.qinys.platform.mobile.controller;
+
+import cn.qinys.platform.base.response.Result;
+import cn.qinys.platform.mobile.req.ChatHistoryReq;
+import cn.qinys.platform.mobile.req.ChatReq;
+import cn.qinys.platform.mobile.resp.ChatResp;
+import cn.qinys.platform.mobile.service.ChatService;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import cn.qinys.platform.mobile.resp.ChatHistoryResp;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/dgapi/mobile/chat")
+public class ChatController {
+
+    @Resource
+    private ChatService chatService;
+
+    /**
+     * 发送消息
+     */
+    @PostMapping
+    public Result<ChatResp> chat(@RequestBody @Valid ChatReq req) {
+        String reply = chatService.chat(req);
+        ChatResp resp = new ChatResp();
+        resp.setReply(reply);
+        return new Result<>(resp);
+    }
+
+    /**
+     * 获取聊天历史(按许愿树过滤)
+     */
+    @GetMapping("/history")
+    public Result<Page<ChatHistoryResp>> history(@Valid ChatHistoryReq req) {
+        Page<ChatHistoryResp> resp = chatService.getHistory(req);
+        return new Result<>(resp);
+    }
+}

+ 1 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/TreeController.java

@@ -1,6 +1,7 @@
 package cn.qinys.platform.mobile.controller;
 
 import cn.qinys.platform.base.response.Result;
+import cn.qinys.platform.mobile.req.TreeCreateReq;
 import cn.qinys.platform.mobile.req.TreeDetailReq;
 import cn.qinys.platform.mobile.req.WishingTreeListReq;
 import cn.qinys.platform.mobile.resp.TreeDetailResp;

+ 18 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/mapper/ChatMessageMapper.java

@@ -0,0 +1,18 @@
+package cn.qinys.platform.mobile.mapper;
+
+import cn.qinys.platform.entity.wishing.ChatMessage;
+import cn.qinys.platform.mobile.resp.ChatHistoryResp;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface ChatMessageMapper extends BaseMapper<ChatMessage> {
+    @Select("SELECT * FROM chat_message ${ew.customSqlSegment}")
+    Page<ChatHistoryResp> getChatHistoryPage(Page<ChatMessage> page, @Param(Constants.WRAPPER) Wrapper<ChatMessage> wrapper);
+
+}

+ 14 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/mapper/SpringAiChatMemoryMapper.java

@@ -0,0 +1,14 @@
+package cn.qinys.platform.mobile.mapper;
+
+import cn.qinys.platform.entity.wishing.SpringAiChatMemory;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-14 11:46
+ **/
+@Mapper
+public interface SpringAiChatMemoryMapper extends BaseMapper<SpringAiChatMemory> {
+}

+ 23 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/ChatHistoryReq.java

@@ -0,0 +1,23 @@
+package cn.qinys.platform.mobile.req;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-14 10:22
+ **/
+@Data
+public class ChatHistoryReq implements Serializable {
+
+    private Integer page = 1;
+
+    private Integer pageSize = 10;
+
+    @NotNull(message = "tree id can not be null")
+    private Integer treeId;
+
+}

+ 20 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/ChatReq.java

@@ -0,0 +1,20 @@
+package cn.qinys.platform.mobile.req;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class ChatReq implements Serializable {
+
+    @NotBlank(message = "消息不能为空")
+    private String message;
+
+    /** 许愿树 ID(可选,用于关联上下文) */
+    private Integer treeId;
+
+    /** 会话 ID,用于关联上下文记忆 */
+    @NotBlank(message = "conversationId 不能为空")
+    private String conversationId;
+}

+ 2 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/LoginReq.java

@@ -2,6 +2,7 @@ package cn.qinys.platform.mobile.req;
 
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -17,6 +18,7 @@ public class LoginReq implements Serializable {
      * 手机号 加密传输
      */
     @NotBlank(message = "电话号码不能为空")
+    @Pattern(regexp = "^(?:\\+?86)?1[3-9]\\d{9}$", message = "手机号格式不正确")
     private String mobile;
 
     /**

+ 19 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/TreeAddressReq.java

@@ -0,0 +1,19 @@
+package cn.qinys.platform.mobile.req;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class TreeAddressReq implements Serializable {
+
+
+    @NotNull(message = "经度不能为空")
+    private BigDecimal longitude;
+
+    @NotNull(message = "纬度不能为空")
+    private BigDecimal latitude;
+
+}

+ 29 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/TreeCreateReq.java

@@ -0,0 +1,29 @@
+package cn.qinys.platform.mobile.req;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class TreeCreateReq implements Serializable {
+
+    @NotBlank(message = "名称不能为空")
+    private String name;
+
+    private String description;
+
+    @NotNull(message = "经度不能为空")
+    private BigDecimal longitude;
+
+    @NotNull(message = "纬度不能为空")
+    private BigDecimal latitude;
+
+    private String address;
+
+    private Integer radius = 100;
+
+    private String coverImage;
+}

+ 2 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/WishCreateReq.java

@@ -30,4 +30,6 @@ public class WishCreateReq implements Serializable {
     private Boolean isPublic = true;
 
     private List<String> tags;
+
+    private Integer type;
 }

+ 18 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/ChatHistoryResp.java

@@ -0,0 +1,18 @@
+package cn.qinys.platform.mobile.resp;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+public class ChatHistoryResp implements Serializable {
+
+    private String id;
+    private String role;
+    private String content;
+    private String type;
+    @JsonFormat(pattern = "MM-dd HH:mm")
+    private LocalDateTime createdAt;
+}

+ 11 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/ChatResp.java

@@ -0,0 +1,11 @@
+package cn.qinys.platform.mobile.resp;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class ChatResp implements Serializable {
+
+    private String reply;
+}

+ 2 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/LoginResp.java

@@ -20,4 +20,6 @@ public class LoginResp implements Serializable {
 
     private String avatar;
 
+    private Integer isAdmin;
+
 }

+ 19 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/TreeAddressResp.java

@@ -0,0 +1,19 @@
+package cn.qinys.platform.mobile.resp;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-15 19:29
+ **/
+@Data
+public class TreeAddressResp implements Serializable {
+
+    private String address;
+
+    private String description;
+
+}

+ 22 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/ChatService.java

@@ -0,0 +1,22 @@
+package cn.qinys.platform.mobile.service;
+
+import cn.qinys.platform.mobile.req.ChatHistoryReq;
+import cn.qinys.platform.mobile.req.ChatReq;
+import cn.qinys.platform.mobile.resp.ChatHistoryResp;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+
+/**
+ * 智能聊天客服服务
+ */
+public interface ChatService {
+
+    /**
+     * 发送消息并获取 AI 回复(自动保存聊天记录,基于 conversationId 记忆上下文)
+     */
+    String chat(ChatReq req);
+
+    /**
+     * 获取当前用户在某棵树下的聊天历史
+     */
+    Page<ChatHistoryResp> getHistory(ChatHistoryReq req);
+}

+ 17 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/ImageService.java

@@ -0,0 +1,17 @@
+package cn.qinys.platform.mobile.service;
+
+import cn.qinys.platform.entity.wishing.UserWish;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-13 23:46
+ **/
+public interface ImageService {
+
+
+    String getImageUrl(String msg);
+
+    void appendWishImage(UserWish wish);
+
+}

+ 10 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/TreeService.java

@@ -1,7 +1,10 @@
 package cn.qinys.platform.mobile.service;
 
+import cn.qinys.platform.mobile.req.TreeAddressReq;
+import cn.qinys.platform.mobile.req.TreeCreateReq;
 import cn.qinys.platform.mobile.req.TreeDetailReq;
 import cn.qinys.platform.mobile.req.WishingTreeListReq;
+import cn.qinys.platform.mobile.resp.TreeAddressResp;
 import cn.qinys.platform.mobile.resp.TreeDetailResp;
 import cn.qinys.platform.mobile.resp.TreeListResp;
 
@@ -25,4 +28,11 @@ public interface TreeService {
      * 查询许愿树详情
      */
     TreeDetailResp getTreeDetail(TreeDetailReq req);
+
+    /**
+     * 创建许愿树(管理员功能)
+     */
+    void create(TreeCreateReq req);
+
+    TreeAddressResp getAddress(TreeAddressReq req);
 }

+ 5 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/AbstractUserWishService.java

@@ -1,7 +1,11 @@
 package cn.qinys.platform.mobile.service.impl;
 
 import cn.qinys.platform.entity.wishing.WishingTreeExtension;
+import cn.qinys.platform.mobile.mapper.ChatMessageMapper;
+import cn.qinys.platform.mobile.mapper.SpringAiChatMemoryMapper;
+import cn.qinys.platform.mobile.mapper.UserWishMapper;
 import cn.qinys.platform.mobile.mapper.WishingTreeExtensionMapper;
+import cn.qinys.platform.mobile.service.ImageService;
 import jakarta.annotation.Resource;
 
 /**
@@ -32,4 +36,5 @@ public abstract class AbstractUserWishService {
         treeExtensionMapper.insertOrUpdate(extension);
     }
 
+
 }

+ 87 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/ChatServiceImpl.java

@@ -0,0 +1,87 @@
+package cn.qinys.platform.mobile.service.impl;
+
+import cn.qinys.platform.base.security.utils.CurrentUtils;
+import cn.qinys.platform.entity.wishing.ChatMessage;
+import cn.qinys.platform.mobile.constants.MsgTypeEnum;
+import cn.qinys.platform.mobile.mapper.ChatMessageMapper;
+import cn.qinys.platform.mobile.req.ChatHistoryReq;
+import cn.qinys.platform.mobile.req.ChatReq;
+import cn.qinys.platform.mobile.resp.ChatHistoryResp;
+import cn.qinys.platform.mobile.service.ChatService;
+import cn.qinys.platform.mobile.tool.DateTimeTools;
+import cn.qinys.platform.mobile.tool.WeatherTools;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+public class ChatServiceImpl implements ChatService {
+
+    @Resource
+    private ChatClient chatClient;
+
+    @Resource
+    private ChatMessageMapper chatMessageMapper;
+
+
+    @Override
+    public String chat(ChatReq req) {
+        String userId = getUserId();
+        if (!"0".equals(userId)) {
+            req.setConversationId(userId);
+        }
+        // 保存用户消息
+        saveMessage(userId, req.getTreeId(), "user", req.getMessage(), MsgTypeEnum.TEXT.getValue());
+        String reply;
+        try {
+            reply = chatClient.prompt()
+                    .user(req.getMessage())
+                    .tools(new DateTimeTools(), new WeatherTools(req.getTreeId()))
+                    .advisors(a -> a.param("chat_memory_conversation_id", req.getConversationId()))
+                    .call()
+                    .content();
+        } catch (Exception e) {
+            log.error("AI chat error", e);
+            reply = "抱歉,我暂时无法回复,请稍后再试~";
+        }
+
+        // 保存 AI 回复
+        saveMessage(userId, req.getTreeId(), "ai", reply, MsgTypeEnum.TEXT.getValue());
+
+        return reply;
+    }
+
+    // ...existing code...
+
+    @Override
+    public Page<ChatHistoryResp> getHistory(ChatHistoryReq req) {
+        String userId = getUserId();
+        if ("0".equals(userId)) {
+            return new Page<>(0, 0);
+        }
+        Page<ChatMessage> page = new Page<>(req.getPage(), req.getPageSize());
+        QueryWrapper<ChatMessage> wrapper = new QueryWrapper<>();
+        wrapper.eq("tree_id", req.getTreeId());
+        wrapper.eq("user_id", userId);
+        wrapper.orderByDesc("created_at");
+        return chatMessageMapper.getChatHistoryPage(page, wrapper);
+    }
+
+    private void saveMessage(String userId, Integer treeId, String role, String content, String type) {
+        ChatMessage msg = new ChatMessage();
+        msg.setUserId(userId);
+        msg.setTreeId(treeId);
+        msg.setRole(role);
+        msg.setContent(content);
+        msg.setType(type);
+        chatMessageMapper.insert(msg);
+    }
+
+    private String getUserId() {
+        return CurrentUtils.getCurrentUserId();
+    }
+}

+ 115 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/ImageServiceImpl.java

@@ -0,0 +1,115 @@
+package cn.qinys.platform.mobile.service.impl;
+
+import cn.qinys.platform.entity.wishing.ChatMessage;
+import cn.qinys.platform.entity.wishing.SpringAiChatMemory;
+import cn.qinys.platform.entity.wishing.UserWish;
+import cn.qinys.platform.mobile.constants.MsgTypeEnum;
+import cn.qinys.platform.mobile.mapper.ChatMessageMapper;
+import cn.qinys.platform.mobile.mapper.SpringAiChatMemoryMapper;
+import cn.qinys.platform.mobile.mapper.UserWishMapper;
+import cn.qinys.platform.mobile.service.ImageService;
+import cn.qinys.platform.mobile.util.QwenImage;
+import cn.qinys.platform.properties.SystemProperties;
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @author lie tan
+ * @description 图片服务:调用 AI 生成图片,下载到本地目录,返回本地访问 URL
+ * @date 2026-06-13 23:46
+ **/
+@Slf4j
+@Service
+public class ImageServiceImpl implements ImageService {
+
+    @Resource
+    private SystemProperties systemProperties;
+
+    @Resource
+    SpringAiChatMemoryMapper memoryMapper;
+    @Resource
+    UserWishMapper userWishMapper;
+    @Resource
+    ChatMessageMapper messageMapper;
+
+    @Override
+    @Async("mobileTaskExecutor")
+    public void appendWishImage(UserWish wish) {
+        QueryWrapper<SpringAiChatMemory> wrapper = new QueryWrapper<>();
+        wrapper.eq("conversation_id", wish.getUserId())
+                .orderByDesc("timestamp")
+                .last("limit 10");
+        List<SpringAiChatMemory> memoryList = memoryMapper.selectList(wrapper);
+        if (memoryList == null || memoryList.isEmpty()) {
+            return;
+        }
+
+        String sb = "请根据用户许下的愿望,以及最近几轮对话的内容,生成一张与许愿相关的图片,图片不要太大,控制在500k以内。用户的愿望为" +
+                "content:" + wish.getContent() +
+                "tag:" + wish.getTags() +
+                "最近的聊天记录内容为:" + JSON.toJSONString(memoryList);
+
+        String imageUrl = this.getImageUrl(sb);
+
+        if (StringUtils.hasText(imageUrl)) {
+            wish.setImages(JSON.toJSONString(List.of(imageUrl)));
+            userWishMapper.updateById(wish);
+            ChatMessage chatMessage = new ChatMessage();
+            chatMessage.setUserId(wish.getUserId());
+            chatMessage.setTreeId(wish.getTreeId());
+            chatMessage.setType(MsgTypeEnum.TEXT.getValue());
+            chatMessage.setContent("小愿已为您的愿望生成了一张图片,您可以到许愿树下查看哦~~");
+            chatMessage.setRole("ai");
+            messageMapper.insert(chatMessage);
+        }
+
+
+    }
+
+
+    @Override
+    public String getImageUrl(String msg) {
+        // 调用 Qwen 生成图片
+        String imageUrl = QwenImage.call(msg);
+        if (imageUrl == null) {
+            log.error("QwenImage.call returned null");
+            return null;
+        }
+        log.info("Qwen generated image url: {}", imageUrl);
+
+        // 下载图片到本地目录
+        try {
+            String ym = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
+            String fileName = UUID.randomUUID().toString().replace("-", "") + ".png";
+            Path targetDir = Path.of(systemProperties.getFileDirectory(), ym);
+            Files.createDirectories(targetDir);
+            Path targetFile = targetDir.resolve(fileName);
+
+            try (InputStream in = URI.create(imageUrl).toURL().openStream()) {
+                Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING);
+            }
+
+            String localUrl = systemProperties.getBaseUrl() + ym + "/" + fileName;
+            log.info("Image saved to: {}", localUrl);
+            return localUrl;
+        } catch (Exception e) {
+            log.error("Failed to download image from: {}", imageUrl, e);
+            return imageUrl;
+        }
+    }
+}

+ 42 - 1
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/LoginServiceImpl.java

@@ -23,6 +23,7 @@ import org.springframework.util.StringUtils;
 import java.time.LocalDateTime;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.atomic.AtomicReference;
 
 /**
@@ -60,7 +61,8 @@ public class LoginServiceImpl extends AbstractLoginService implements LoginServi
 
         WishingUser user = userMapper.selectByMobile(query.getMobile());
         if (user == null) {
-            throw new BizException("用户不存在");
+            //throw new BizException("用户不存在");
+            user = this.createWishingUser(query.getMobile());
         }
         user.setLastLoginAt(LocalDateTime.now());
         user.setLoginCount(user.getLoginCount() + 1);
@@ -82,6 +84,7 @@ public class LoginServiceImpl extends AbstractLoginService implements LoginServi
         resp.setMobile(user.getMobile());
         resp.setNickname(user.getNickname());
         resp.setAvatar(user.getAvatar());
+        resp.setIsAdmin(user.getIsAdmin());
         return resp;
     }
 
@@ -116,4 +119,42 @@ public class LoginServiceImpl extends AbstractLoginService implements LoginServi
         Integer userId = Integer.valueOf(CurrentUtils.getCurrentUserId());
         return wishMapper.selectWishSummary(userId);
     }
+
+
+    private WishingUser createWishingUser(String mobile) {
+        WishingUser user = new WishingUser();
+        user.setMobile(mobile);
+        user.setNickname(this.createRandUerName());
+        user.setLoginCount(0);
+        user.setAvatar("");
+        user.setIsAdmin(1);
+        userMapper.insert(user);
+        return user;
+    }
+
+    public String createRandUerName() {
+        // 常见单字姓氏列表(可根据需要扩充)
+        String[] surnames = new String[]{"李","王","张","刘","陈","杨","黄","赵","吴","周","徐","孙","胡","朱","高","林","何","郭","马","罗","梁","宋","郑","谢","唐"};
+
+        // 常见单字名字用字(男女通用,列表可扩充)
+        String[] nameChars = new String[]{
+                "伟","芳","娜","秀","英","敏","静","丽","强","磊",
+                "洋","勇","艳","杰","娟","涛","明","超","平","刚",
+                "桂","雪","飞","琳","玲","丹","萍","辉","建","华",
+                "晶","燕","晨","晨","鑫","彤","坤","怡","珊","瑶"
+        };
+
+        ThreadLocalRandom rnd = ThreadLocalRandom.current();
+        String surname = surnames[rnd.nextInt(surnames.length)];
+
+        // 给名长度 1 或 2
+        int givenLen = rnd.nextInt(1, 3); // 1 或 2
+        StringBuilder givenName = new StringBuilder();
+        for (int i = 0; i < givenLen; i++) {
+            givenName.append(nameChars[rnd.nextInt(nameChars.length)]);
+        }
+
+        return surname + givenName;
+    }
+
 }

+ 60 - 2
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/TreeServiceImpl.java

@@ -1,21 +1,31 @@
 package cn.qinys.platform.mobile.service.impl;
 
+import cn.qinys.platform.base.response.Result;
 import cn.qinys.platform.base.security.utils.CurrentUtils;
+import cn.qinys.platform.entity.wishing.WishingTree;
 import cn.qinys.platform.mobile.mapper.UserWishMapper;
 import cn.qinys.platform.mobile.mapper.WishingTreeMapper;
+import cn.qinys.platform.mobile.req.TreeAddressReq;
+import cn.qinys.platform.mobile.req.TreeCreateReq;
 import cn.qinys.platform.mobile.req.TreeDetailReq;
 import cn.qinys.platform.mobile.req.WishingTreeListReq;
+import cn.qinys.platform.mobile.resp.TreeAddressResp;
 import cn.qinys.platform.mobile.resp.TreeDetailResp;
 import cn.qinys.platform.mobile.resp.TreeListResp;
 import cn.qinys.platform.mobile.service.TreeService;
 import cn.qinys.platform.mobile.util.CoordUtil;
+import cn.qinys.platform.openfeign.UpmsFeignClient;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
 /**
@@ -31,6 +41,10 @@ public class TreeServiceImpl implements TreeService {
     private WishingTreeMapper treeMapper;
     @Resource
     UserWishMapper wishMapper;
+    @Resource
+    UpmsFeignClient upmsFeignClient;
+    @Resource
+    ChatClient chatClient;
 
     private static final int EARTH_RADIUS = 6371000; // 地球半径(米)
 
@@ -79,14 +93,58 @@ public class TreeServiceImpl implements TreeService {
         return (int) Math.round(EARTH_RADIUS * c);
     }
 
-    private List<Integer> userTrees(){
+
+    @Override
+    public void create(TreeCreateReq req) {
+        // 前端传入的是 WGS-84 坐标(天地图 img_w),DB 存储 GCJ-02,需转换
+        double[] gcj = CoordUtil.wgs84ToGcj02(req.getLongitude().doubleValue(), req.getLatitude().doubleValue());
+        TreeAddressReq addressReq = new TreeAddressReq();
+        addressReq.setLatitude(BigDecimal.valueOf(gcj[1]));
+        addressReq.setLongitude(BigDecimal.valueOf(gcj[0]));
+        String address = upmsFeignClient.getAddressByAltitudeAndLongitude(addressReq).getData();
+        String description  = this.getDescription(address);
+        WishingTree tree = new WishingTree();
+        tree.setName(req.getName());
+        tree.setAddress(address);
+        tree.setDescription(description);
+        tree.setSummary(description);
+        tree.setLongitude(BigDecimal.valueOf(gcj[0]));
+        tree.setLatitude(BigDecimal.valueOf(gcj[1]));
+        tree.setRadius(req.getRadius() != null ? req.getRadius() : 100);
+        tree.setCoverImage(req.getCoverImage());
+        tree.setIsActive(1);
+        treeMapper.insert(tree);
+    }
+
+    @Override
+    public TreeAddressResp getAddress(TreeAddressReq req) {
+        String address = upmsFeignClient.getAddressByAltitudeAndLongitude(req).getData();
+        TreeAddressResp addressResp = new TreeAddressResp();
+        addressResp.setAddress(address);
+        addressResp.setDescription(this.getDescription(address));
+        return addressResp;
+    }
+
+    private List<Integer> userTrees() {
         Integer userId = Integer.valueOf(CurrentUtils.getCurrentUserId());
         List<Integer> trees = wishMapper.selectUserTrees(userId);
-        if(trees == null || trees.isEmpty()){
+        if (trees == null || trees.isEmpty()) {
             trees = new ArrayList<>();
             trees.add(0);
         }
         return trees;
 
     }
+
+    private String getDescription(String address) {
+        return chatClient.prompt("""
+                        你是一个人文地理知识库助手,用户给你一个地址,你根据地址信息给出200 - 300字左右的描述。
+                        如果地址是一座城市,给出城市的描述信息如历史,人文景观等;
+                        如果是一个小区,简单介绍小区的信息,如开发商,小区建成时间,小区周边等;
+                        如果是一个公司,介绍公司的情况,如果是一个旅游景点,也请给出一些描述信息
+                        """).user(address)
+                .advisors(a -> a.param("chat_memory_conversation_id", UUID.randomUUID().toString()))
+                .call().content();
+    }
+
 }

+ 10 - 1
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/UserWishServiceImpl.java

@@ -3,12 +3,14 @@ package cn.qinys.platform.mobile.service.impl;
 import cn.qinys.platform.base.security.utils.CurrentUtils;
 import cn.qinys.platform.entity.wishing.UserWish;
 import cn.qinys.platform.entity.wishing.WishingTree;
+import cn.qinys.platform.mobile.constants.WishtypeEnum;
 import cn.qinys.platform.mobile.mapper.UserWishMapper;
 import cn.qinys.platform.mobile.mapper.WishingTreeMapper;
 import cn.qinys.platform.mobile.req.WishCreateReq;
 import cn.qinys.platform.mobile.req.WishPageReq;
 import cn.qinys.platform.mobile.resp.WishDetailResp;
 import cn.qinys.platform.mobile.resp.WishPageResp;
+import cn.qinys.platform.mobile.service.ImageService;
 import cn.qinys.platform.mobile.service.UserWishService;
 import com.alibaba.fastjson2.JSON;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -28,6 +30,9 @@ public class UserWishServiceImpl extends AbstractUserWishService implements User
     @Resource
     private WishingTreeMapper treeMapper;
 
+    @Resource
+    private ImageService imageService;
+
 
     @Override
     public Page<WishPageResp> pageByTree(Integer treeId, WishPageReq req) {
@@ -56,7 +61,7 @@ public class UserWishServiceImpl extends AbstractUserWishService implements User
         return wishMapper.selectWishDetail(id);
     }
 
-    @Transactional(rollbackFor = Exception.class)
+
     @Override
     public void create(WishCreateReq req) {
         WishingTree tree = treeMapper.selectById(req.getTreeId());
@@ -71,8 +76,12 @@ public class UserWishServiceImpl extends AbstractUserWishService implements User
         wish.setAddress(req.getAddress());
         wish.setIsPublic(req.getIsPublic() != null && req.getIsPublic() ? 1 : 0);
         wish.setTags(JSON.toJSONString(req.getTags()));
+        wish.setType(req.getType());
         wishMapper.insert(wish);
         this.treeWishesCounter(req.getTreeId(), wish.getIsPublic());
+        if (WishtypeEnum.AUTO.getValue().equals(req.getType())) {
+            imageService.appendWishImage(wish);
+        }
     }
 
     @Override

+ 24 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/tool/DateTimeTools.java

@@ -0,0 +1,24 @@
+package cn.qinys.platform.mobile.tool;
+
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.context.i18n.LocaleContextHolder;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-13 10:20
+ **/
+public class DateTimeTools {
+
+
+    @Tool(description = "Get the current date and time in the user's timezone")
+    String getCurrentDateTime() {
+        return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
+    }
+
+
+
+
+}

+ 45 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/tool/WeatherTools.java

@@ -0,0 +1,45 @@
+package cn.qinys.platform.mobile.tool;
+
+import cn.qinys.platform.base.utils.SpringUtil;
+import cn.qinys.platform.entity.wishing.WishingTreeExtension;
+import cn.qinys.platform.mobile.mapper.WishingTreeExtensionMapper;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-13 10:47
+ **/
+
+public class WeatherTools {
+
+    String key = "88a0c82c3f60a748c6c638ef9db16bca";
+    String url = "https://restapi.amap.com/v3/weather/weatherInfo?city={0}&key={1}";
+
+    private Integer treeId;
+
+    public WeatherTools() {
+    }
+
+    public WeatherTools(Integer treeId) {
+        this.treeId = treeId;
+    }
+
+
+    @Tool(description = """
+            get the weather forecast for the special wishing tree
+            along with the current date and time in the user's timezone
+            """)
+    public String getWeather() {
+        WishingTreeExtensionMapper extensionMapper = SpringUtil.getBean(WishingTreeExtensionMapper.class);
+        WishingTreeExtension extension = extensionMapper.selectByTreeId(treeId);
+        if (extension == null || extension.getAdcode() == null) {
+            return null;
+        }
+        RestTemplate restTemplate = SpringUtil.getBean(RestTemplate.class);
+        return restTemplate.getForObject(url, String.class, extension.getAdcode(), key);
+    }
+
+
+}

+ 62 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/util/QwenImage.java

@@ -0,0 +1,62 @@
+package cn.qinys.platform.mobile.util;
+
+import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation;
+import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam;
+import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult;
+import com.alibaba.dashscope.common.MultiModalMessage;
+import com.alibaba.dashscope.common.Role;
+import com.alibaba.dashscope.utils.Constants;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class QwenImage {
+
+    static {
+        // 以下为华北2(北京)地域的URL,各地域的URL不同。
+        Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1";
+    }
+
+    // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
+    static String apiKey = "sk-a52acbfc944e495ead45b1c54b5659fb";
+
+    /**
+     * String msg = """
+     * 我的名字叫400块,是一只5岁的英短小猫,我喜欢吃鸡肉冻干
+     * 我现在在一个许愿树下许愿,希望我身体健康,天天都有冻干吃,吃多了也不长胖
+     * """;
+     *
+     * @param msg
+     * @return
+     */
+    public static String call(String msg) {
+        MultiModalConversation conv = new MultiModalConversation();
+        MultiModalMessage userMessage = MultiModalMessage.builder().role(Role.USER.getValue())
+                .content(List.of(Collections.singletonMap("text", msg))).build();
+        Map<String, Object> parameters = new HashMap<>();
+        parameters.put("watermark", false);
+        parameters.put("prompt_extend", true);
+        parameters.put("negative_prompt", "低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。");
+        parameters.put("size", "720*720");
+
+        MultiModalConversationParam param = MultiModalConversationParam.builder()
+                .apiKey(apiKey)
+                .model("qwen-image-2.0-pro")
+                .messages(Collections.singletonList(userMessage))
+                .parameters(parameters)
+                .build();
+        try {
+            MultiModalConversationResult result = conv.call(param);
+            return result.getOutput().getChoices().getFirst().getMessage().getContent().getFirst().get("image").toString();
+        } catch (Exception e) {
+            log.error("get QwenImage error", e);
+        }
+        return null;
+    }
+
+
+}

+ 31 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/openfeign/UpmsFeignClient.java

@@ -0,0 +1,31 @@
+package cn.qinys.platform.openfeign;
+
+
+import cn.qinys.platform.base.constants.ServiceConstants;
+import cn.qinys.platform.base.response.Result;
+import cn.qinys.platform.config.FeignConfiguration;
+import cn.qinys.platform.mobile.req.TreeAddressReq;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-05-24 14:12
+ **/
+@Component
+@FeignClient(name = ServiceConstants.UPMS_SERVICE,
+        configuration = FeignConfiguration.class)
+public interface UpmsFeignClient {
+
+
+    /**
+     * 根据经纬度获取地址信息
+     *
+     * @return 无
+     */
+    @PostMapping("/upms/gaodekey/address")
+    Result<String> getAddressByAltitudeAndLongitude(@RequestBody TreeAddressReq req);
+}

+ 7 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/resources/application-dev.yml

@@ -2,6 +2,13 @@ logging:
   level:
     cn.qinys.platform: debug
 spring:
+  ai:
+    deepseek:
+      api-key: sk-7de46bc6a5c845fcb5ec060e5e3f1441
+      chat:
+        options:
+          model: deepseek-chat
+          temperature: 0.7
   cloud:
     nacos:
       discovery:

+ 7 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/resources/application-local.yml

@@ -2,6 +2,13 @@ logging:
   level:
     cn.qinys.platform: debug
 spring:
+  ai:
+    deepseek:
+      api-key: sk-7de46bc6a5c845fcb5ec060e5e3f1441
+      chat:
+        options:
+          model: deepseek-chat
+          temperature: 0.7
   cloud:
     nacos:
       discovery:

+ 17 - 3
wishing-platform/platform-service/platform-service-mobile/src/main/resources/application.yml

@@ -7,6 +7,12 @@ spring:
     multipart:
       max-file-size: 50MB
       max-request-size: 50MB
+  ai:
+    chat:
+      memory:
+        repository:
+          jdbc:
+            initialize-schema: never
 
   application:
     name: wishing-mobile
@@ -15,11 +21,19 @@ spring:
 
 jwt:
   ignore-urls:
-    - /**
+    - /dgapi/mobile/user/logout
+    - /dgapi/mobile/user/login
+    - /dgapi/mobile/user/login/captcha
+    - /dgapi/mobile/wishingtree/list
+    - /dgapi/mobile/wishingtree/detail
+    - /dgapi/mobile/chat/history
+    - /dgapi/mobile/chat
+    - /dgapi/mobile/wish/tree/**
+    - /dgapi/mobile/ext/file/**
 
 captcha:
-    width: 100
-    height: 40
+  width: 100
+  height: 40
 
 
 

+ 117 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/resources/logback-spring.xml

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <contextName>logback</contextName>
+
+    <property name="LOG_HOME" value="./logs/mobile"/>
+    <springProperty scope="context" name="applicationName" source="spring.application.name" defaultValue="localhost" />
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>debug</level>
+        </filter>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [${applicationName},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-}] [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <!--2. 输出到文档-->
+    <!-- 2.1 level为 DEBUG 日志,时间滚动输出  -->
+    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 正在记录的日志文档的路径及文档名 -->
+        <file>${LOG_HOME}/debug.log</file>
+        <!--日志文档输出格式-->
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset> <!-- 设置字符集 -->
+        </encoder>
+        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志归档 -->
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <!--日志文档保留天数-->
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <!-- 此日志文档只记录debug级别的 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>debug</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 2.2 level为 INFO 日志,时间滚动输出  -->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/info.log</file>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>info</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 2.3 level为 WARN 日志,时间滚动输出  -->
+    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/warn.log</file>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>warn</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/error.log</file>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <springProfile name="dev">
+        <logger name="com.sdcm.pmp" level="debug"/>
+    </springProfile>
+
+    <root level="info">
+        <appender-ref ref="CONSOLE"/>
+        <appender-ref ref="DEBUG_FILE"/>
+        <appender-ref ref="INFO_FILE"/>
+        <appender-ref ref="WARN_FILE"/>
+        <appender-ref ref="ERROR_FILE"/>
+    </root>
+</configuration>

+ 28 - 0
wishing-platform/platform-service/platform-service-mobile/src/test/java/cn/qinys/platform/mobile/service/impl/UserWishServiceImplTest.java

@@ -0,0 +1,28 @@
+package cn.qinys.platform.mobile.service.impl;
+
+import cn.qinys.platform.mobile.mapper.UserWishMapper;
+import cn.qinys.platform.mobile.service.ImageService;
+import jakarta.annotation.Resource;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-14 12:04
+ **/
+@SpringBootTest
+class UserWishServiceImplTest {
+
+    @Resource
+    ImageService imageService;
+    @Resource
+    UserWishMapper userWishMapper;
+
+    @Test
+    void appendWishImage() {
+
+    }
+
+
+}

+ 23 - 0
wishing-platform/platform-service/platform-service-mobile/src/test/java/cn/qinys/platform/mobile/tool/WeatherToolsTest.java

@@ -0,0 +1,23 @@
+package cn.qinys.platform.mobile.tool;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+/**
+* @author lie tan
+* @description 
+* @date 2026-06-13 11:58
+**/
+
+@SpringBootTest
+class WeatherToolsTest {
+
+
+    @Test
+    void getWeather() {
+        WeatherTools weatherTools = new WeatherTools(3);
+        String weather = weatherTools.getWeather();
+        System.out.println(weather);
+    }
+
+
+}

+ 1 - 1
wishing-platform/platform-service/platform-service-upms/pom.xml

@@ -41,7 +41,7 @@
         </dependency>
         <dependency>
             <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
         </dependency>
         <dependency>
             <groupId>com.baomidou</groupId>

+ 7 - 1
wishing-platform/platform-service/platform-service-upms/src/main/java/cn/qinys/platform/upms/api/controller/GaoDeKeyController.java

@@ -5,6 +5,7 @@ import cn.qinys.platform.base.security.annotation.HasPermission;
 import cn.qinys.platform.base.utils.excel.ExcelElement;
 import cn.qinys.platform.base.utils.excel.ExcelUtils;
 import cn.qinys.platform.upms.api.req.GaoDeGetAddressReq;
+import com.alibaba.fastjson2.JSONObject;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import cn.qinys.platform.upms.api.req.GaoDeKeyPageReq;
 import cn.qinys.platform.upms.api.resp.GaoDeKeyPageResp;
@@ -70,7 +71,7 @@ public class GaoDeKeyController extends DevBaseController {
 
 
     /**
-     * 网关日志分页
+     * 根据经纬度获取地址
      *
      * @return
      */
@@ -80,5 +81,10 @@ public class GaoDeKeyController extends DevBaseController {
         return new Result<>(address);
     }
 
+    @PostMapping("/info")
+    public JSONObject getInfo(@RequestBody @Valid GaoDeGetAddressReq req) {
+        return gaoDeKeyService.getGaoDeResult("", req.getLongitude(), req.getLatitude(), 10);
+    }
+
 
 }

+ 6 - 0
wishing-platform/platform-service/platform-service-upms/src/main/java/cn/qinys/platform/upms/api/service/IGaoDeKeyService.java

@@ -1,10 +1,13 @@
 package cn.qinys.platform.upms.api.service;
 
 import cn.qinys.platform.upms.api.req.GaoDeGetAddressReq;
+import com.alibaba.fastjson2.JSONObject;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import cn.qinys.platform.upms.api.req.GaoDeKeyPageReq;
 import cn.qinys.platform.upms.api.resp.GaoDeKeyPageResp;
 
+import java.math.BigDecimal;
+
 /**
  * 网关日志
  *
@@ -27,4 +30,7 @@ public interface IGaoDeKeyService {
      * @return
      */
     String getAddress(GaoDeGetAddressReq req);
+
+
+    JSONObject getGaoDeResult(String key, BigDecimal lon, BigDecimal lat, Integer index);
 }

+ 5 - 5
wishing-platform/platform-service/platform-service-upms/src/main/java/cn/qinys/platform/upms/api/service/impl/GaoDeKeyServiceImpl.java

@@ -57,11 +57,12 @@ public class GaoDeKeyServiceImpl implements IGaoDeKeyService {
 
     @Override
     public String getAddress(GaoDeGetAddressReq req) {
-        return this.getGaoDeResult("", req.getLongitude(), req.getLatitude(), 0);
+        JSONObject res = this.getGaoDeResult("", req.getLongitude(), req.getLatitude(), 0);
+        return res == null ? "" : res.getString("formatted_address");
     }
 
-
-    private String getGaoDeResult(String key, BigDecimal lon, BigDecimal lat, Integer index) {
+    @Override
+    public JSONObject getGaoDeResult(String key, BigDecimal lon, BigDecimal lat, Integer index) {
         if (index >= 100) {
             return null;
         }
@@ -76,9 +77,8 @@ public class GaoDeKeyServiceImpl implements IGaoDeKeyService {
         if ("1".equals(res.getString("status"))) {
             log.info("获取逆地理解析信息成功{}", res);
             JSONObject regeocode = res.getJSONObject("regeocode");
-            String address = regeocode.getString("formatted_address");
             this.countKeyUsage(geoKey.getGeoKey());
-            return address;
+            return regeocode;
         } else {
             return this.getGaoDeResult(geoKey.getGeoKey(), lon, lat, index);
         }

+ 0 - 7
wishing-platform/platform-service/platform-service-upms/src/main/resources/application-dev.yml

@@ -38,10 +38,3 @@ mybatis-plus:
 system:
   base-url: https://wish.qinys.cn/dgapi/upms/ext/file/
   file-directory: /file/
-
-
-sms:
-  debug: true
-  username: xcgsqwhxgl
-  pwd: rJi51O
-  url: http://218.65.241.103:9088/services/WebServiceSoap?wsdl

+ 1 - 7
wishing-platform/platform-service/platform-service-upms/src/main/resources/application-local.yml

@@ -35,13 +35,7 @@ mybatis-plus:
       logic-delete-value: 1
       logic-not-delete-value: 0
 
+
 system:
   base-url: https://wish.qinys.cn/dgapi/upms/ext/file/
   file-directory: /file/
-
-
-sms:
-  debug: true
-  username: xcgsqwhxgl
-  pwd: rJi51O
-  url: http://218.65.241.103:9088/services/WebServiceSoap?wsdl

+ 117 - 0
wishing-platform/platform-service/platform-service-upms/src/main/resources/logback-spring.xml

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <contextName>logback</contextName>
+
+    <property name="LOG_HOME" value="./logs/upms"/>
+    <springProperty scope="context" name="applicationName" source="spring.application.name" defaultValue="localhost" />
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>debug</level>
+        </filter>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [${applicationName},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-}] [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <!--2. 输出到文档-->
+    <!-- 2.1 level为 DEBUG 日志,时间滚动输出  -->
+    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 正在记录的日志文档的路径及文档名 -->
+        <file>${LOG_HOME}/debug.log</file>
+        <!--日志文档输出格式-->
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset> <!-- 设置字符集 -->
+        </encoder>
+        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志归档 -->
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <!--日志文档保留天数-->
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <!-- 此日志文档只记录debug级别的 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>debug</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 2.2 level为 INFO 日志,时间滚动输出  -->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/info.log</file>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>info</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 2.3 level为 WARN 日志,时间滚动输出  -->
+    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/warn.log</file>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>warn</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/error.log</file>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} >====> [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM, aux}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <springProfile name="dev">
+        <logger name="com.sdcm.pmp" level="debug"/>
+    </springProfile>
+
+    <root level="info">
+        <appender-ref ref="CONSOLE"/>
+        <appender-ref ref="DEBUG_FILE"/>
+        <appender-ref ref="INFO_FILE"/>
+        <appender-ref ref="WARN_FILE"/>
+        <appender-ref ref="ERROR_FILE"/>
+    </root>
+</configuration>

+ 16 - 0
wishing-platform/platform-service/platform-service-upms/src/test/java/cn/qinys/platform/upms/api/service/IGaoDeKeyServiceTest.java

@@ -0,0 +1,16 @@
+package cn.qinys.platform.upms.api.service;
+
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-13 11:06
+ **/
+@SpringBootTest
+class IGaoDeKeyServiceTest {
+
+
+    
+
+}

+ 1 - 0
wishing-tree-h5/components.d.ts

@@ -12,6 +12,7 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     BottomNav: typeof import('./src/components/BottomNav.vue')['default']
+    ChatWidget: typeof import('./src/components/ChatWidget.vue')['default']
     ImageUploader: typeof import('./src/components/ImageUploader.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 2 - 1
wishing-tree-h5/src/api/auth.ts

@@ -24,7 +24,7 @@ export async function sendSms(phone: string) {
 }
 
 export async function login(phone: string, code: string, captcha: string, captchaKey: string) {
-  const res = await request<{ token: string; mobile: string; nickname: string; avatar: string }>(
+  const res = await request<{ token: string; mobile: string; nickname: string; avatar: string; isAdmin: number }>(
     '/mobile/user/login',
     {
       method: 'POST',
@@ -36,5 +36,6 @@ export async function login(phone: string, code: string, captcha: string, captch
     phone: res.data.mobile,
     nickname: res.data.nickname,
     avatarUrl: res.data.avatar || '',
+    isAdmin: res.data.isAdmin || 0,
   }
 }

+ 23 - 0
wishing-tree-h5/src/api/chat.ts

@@ -0,0 +1,23 @@
+import { request } from './request'
+
+export interface ChatMsg {
+  role: 'user' | 'ai'
+  content: string
+  type?: string
+  createdAt: string
+}
+
+export async function sendMessage(message: string, conversationId: string, treeId?: number): Promise<string> {
+  const res = await request<{ reply: string }>('/mobile/chat', {
+    method: 'POST',
+    body: JSON.stringify({ message, treeId, conversationId }),
+  })
+  return res.data.reply
+}
+
+export async function fetchHistory(treeId: number, page = 1, pageSize = 10): Promise<{ records: ChatMsg[]; total: number }> {
+  const res = await request<{ records: ChatMsg[]; total: number }>(
+    `/mobile/chat/history?treeId=${treeId}&page=${page}&pageSize=${pageSize}`,
+  )
+  return { records: res.data.records || [], total: res.data.total || 0 }
+}

+ 16 - 3
wishing-tree-h5/src/api/request.ts

@@ -1,4 +1,5 @@
-import { getStorage } from '@/utils/storage'
+import { getStorage, removeStorage } from '@/utils/storage'
+import router from '@/router'
 
 
 export interface ApiResponse<T = any> {
@@ -8,7 +9,6 @@ export interface ApiResponse<T = any> {
 }
 
 function showError(msg: string): Promise<void> {
-  // 不使用 Vant showToast(在非组件上下文中不可靠),直接用 DOM 插入
   const el = document.createElement('div')
   el.textContent = msg
   el.style.cssText =
@@ -21,6 +21,13 @@ function showError(msg: string): Promise<void> {
   return new Promise((resolve) => setTimeout(resolve, 150))
 }
 
+function handleUnauthorized() {
+  removeStorage('token')
+  showError('登录已过期,请重新登录').then(() => {
+    router.push('/login')
+  })
+}
+
 export async function request<T = any>(url: string, options?: RequestInit): Promise<ApiResponse<T>> {
 
     const token = getStorage('token')
@@ -41,6 +48,13 @@ export async function request<T = any>(url: string, options?: RequestInit): Prom
         await showError('网络连接失败,请检查网络')
         throw new Error('网络连接失败')
     }
+
+    // 401 未授权:清除 token 并跳转登录页
+    if (res.status === 401) {
+        handleUnauthorized()
+        throw new Error('未授权')
+    }
+
     // HTTP 状态码错误(500、404 等)
     if (!res.ok) {
         let errMsg = ''
@@ -68,7 +82,6 @@ export async function request<T = any>(url: string, options?: RequestInit): Prom
 
     if (json.code !== 200) {
         const errMsg = json.msg || '请求失败'
-       // await showError(errMsg)
         throw new Error(errMsg)
     }
     return json

+ 16 - 0
wishing-tree-h5/src/api/tree.ts

@@ -25,3 +25,19 @@ export async function fetchTreeDetail(id: number, lng?: number, lat?: number) {
   })
   return res.data
 }
+
+export async function createTree(data: {
+  name: string
+  description: string
+  longitude: number
+  latitude: number
+  address: string
+  radius: number
+  coverImage: string
+}) {
+  const res = await request<any>('/mobile/admin/create/tree', {
+    method: 'POST',
+    body: JSON.stringify(data),
+  })
+  return res.data
+}

+ 1 - 0
wishing-tree-h5/src/api/wish.ts

@@ -54,6 +54,7 @@ export async function submitWish(data: {
   address: string
   isPublic: boolean
   tags: string[]
+  type: number
 }) {
   if (USE_MOCK) return createWishMock(data as any)
 

+ 446 - 0
wishing-tree-h5/src/components/ChatWidget.vue

@@ -0,0 +1,446 @@
+<template>
+  <!-- 悬浮按钮 -->
+  <div class="chat-fab" @click="toggleChat">
+    <van-icon :name="showChat ? 'cross' : 'chat-o'" size="22" color="#fff" />
+  </div>
+
+  <!-- 聊天面板 -->
+  <van-popup
+    v-model:show="showChat"
+    position="bottom"
+    round
+    :style="{ height: '65vh' }"
+    teleport="body"
+  >
+    <div class="chat-panel">
+      <div class="chat-header">
+        <span class="chat-title">🤖 小愿 · 智能客服</span>
+        <van-icon name="cross" size="18" @click="showChat = false" />
+      </div>
+
+      <div class="chat-body" ref="msgBody">
+        <!-- 加载更多历史记录 -->
+        <div v-if="!historyFinished && historyLoaded" class="load-more" @click="loadHistory">
+          <span v-if="historyLoading">加载中...</span>
+          <span v-else>点击加载更多历史</span>
+        </div>
+        <div v-if="messages.length === 0" class="chat-empty">
+          <p>👋 你好,我是许愿树小助手"小愿"</p>
+          <p>有什么关于许愿的问题都可以问我~</p>
+        </div>
+        <div
+          v-for="(msg, i) in messages"
+          :key="i"
+          :class="['chat-msg', msg.role, { 'has-wish': msg.wish }]"
+        >
+          <div v-if="msg.type === 'image'" class="chat-bubble chat-bubble-image">
+            <van-image :src="msg.content" fit="cover" radius="8" style="max-width:200px" />
+          </div>
+          <div v-else class="chat-bubble">{{ msg.content }}</div>
+          <!-- AI 许愿建议卡 -->
+          <div v-if="msg.wish" class="wish-card">
+            <div class="wish-card-header">✨ 愿望概览</div>
+            <p class="wish-card-content">{{ msg.wish.content }}</p>
+            <div class="wish-card-tags" v-if="msg.wish.tags?.length">
+              <van-tag v-for="t in msg.wish.tags" :key="t" plain type="primary" size="medium">
+                {{ t }}
+              </van-tag>
+            </div>
+            <div class="wish-card-actions">
+              <van-button size="small" round plain @click="cancelWish(i)">再想想</van-button>
+              <van-button size="small" round type="primary" :loading="msg.wishSubmitting" @click="confirmWish(i)">
+                确认许愿 🙏
+              </van-button>
+            </div>
+          </div>
+        </div>
+        <div v-if="sending" class="chat-msg ai">
+          <div class="chat-bubble typing">...</div>
+        </div>
+      </div>
+
+      <div class="chat-footer">
+        <van-field
+          v-model="input"
+          type="textarea"
+          rows="1"
+          autosize
+          placeholder="输入你的问题..."
+          :border="false"
+        />
+        <van-button
+          type="primary"
+          size="small"
+          round
+          :disabled="!input.trim() || sending"
+          @click="send"
+        >
+          发送
+        </van-button>
+      </div>
+    </div>
+  </van-popup>
+</template>
+
+<script setup lang="ts">
+import { ref, nextTick, watch } from 'vue'
+import { showToast, showSuccessToast } from 'vant'
+import { sendMessage, fetchHistory } from '@/api/chat'
+import { submitWish } from '@/api/wish'
+import { useUserStore } from '@/stores/user'
+import { useLocationStore } from '@/stores/location'
+
+const props = defineProps<{
+  treeId?: number
+  treeName?: string
+  treeAddress?: string
+  isInRange?: boolean
+  distance?: number
+}>()
+
+interface WishSuggestion {
+  content: string
+  tags: string[]
+}
+
+interface ChatMsg {
+  role: 'user' | 'ai'
+  content: string
+  type?: string
+  wish?: WishSuggestion
+  wishSubmitting?: boolean
+}
+
+const userStore = useUserStore()
+const locationStore = useLocationStore()
+
+const showChat = ref(false)
+const input = ref('')
+const sending = ref(false)
+const messages = ref<ChatMsg[]>([])
+const msgBody = ref<HTMLElement | null>(null)
+const historyLoaded = ref(false)
+const historyLoading = ref(false)
+const historyFinished = ref(false)
+let historyPage = 1
+const conversationId = ref(crypto.randomUUID())
+
+/** 从 AI 回复中提取 [WISH]...[/WISH] 标记,兼容各种格式变体 */
+function parseWish(content: string): { text: string; wish: WishSuggestion | null } {
+  // 尝试多种可能的标记格式
+  const patterns = [
+    /\[WISH\]\s*(\{[\s\S]*?\})\s*\[\/WISH\]/i,
+    /\[WISH\]\s*([\s\S]*?)\s*\[\/WISH\]/i,
+    /\【WISH\】\s*(\{[\s\S]*?\})\s*\【\/WISH\】/i,
+  ]
+  for (const regex of patterns) {
+    const match = content.match(regex)
+    if (match) {
+      try {
+        const wish = JSON.parse(match[1])
+        const text = content.replace(regex, '').trim()
+        if (wish.content) {
+          return { text, wish: { content: wish.content, tags: wish.tags || [] } }
+        }
+      } catch {
+        // 继续尝试下一个模式
+      }
+    }
+  }
+  return { text: content, wish: null }
+}
+
+async function loadHistory() {
+  if (!props.treeId || historyLoading.value || historyFinished.value) return
+  historyLoading.value = true
+  try {
+    const { records, total } = await fetchHistory(props.treeId, historyPage, 15)
+    if (records.length > 0) {
+      // 后端返回 DESC(最新在前),反转后插入消息列表开头
+      const oldMessages = messages.value
+      const historyMsgs = records.reverse().map((m) => ({ role: m.role, content: m.content, type: m.type }))
+      messages.value = [...historyMsgs, ...oldMessages]
+    }
+    historyFinished.value = records.length < 15 || messages.value.length >= total
+    historyPage++
+  } catch {
+    historyFinished.value = true
+  } finally {
+    historyLoading.value = false
+    historyLoaded.value = true
+  }
+}
+
+function toggleChat() {
+  showChat.value = !showChat.value
+}
+
+function scrollToBottom() {
+  nextTick(() => {
+    const el = msgBody.value
+    if (el) {
+      el.scrollTop = el.scrollHeight
+    }
+  })
+}
+
+async function send() {
+  const text = input.value.trim()
+  if (!text || sending.value) return
+
+  messages.value.push({ role: 'user', content: text })
+  input.value = ''
+  scrollToBottom()
+
+  sending.value = true
+  try {
+    const reply = await sendMessage(text, conversationId.value, props.treeId)
+    const { text: cleanText, wish } = parseWish(reply)
+
+    // 如果 AI 生成了一键许愿建议,但用户不在许愿范围内,直接拒绝
+    if (wish && props.isInRange === false) {
+      const distText = props.distance
+        ? (props.distance >= 1000 ? (props.distance / 1000).toFixed(1) + 'km' : props.distance + 'm')
+        : '较远'
+      messages.value.push({
+        role: 'ai',
+        content: `抱歉哦~ 你当前距离许愿树还有 ${distText},超出了许愿范围,暂时无法帮你一键许愿。请先靠近许愿树后再来找我帮忙吧!📿`,
+      })
+    } else {
+      messages.value.push({ role: 'ai', content: cleanText || reply, wish: wish || undefined })
+    }
+  } catch {
+    messages.value.push({ role: 'ai', content: '抱歉,网络出了问题,请稍后再试~' })
+  } finally {
+    sending.value = false
+    scrollToBottom()
+  }
+}
+
+function cancelWish(index: number) {
+  delete messages.value[index].wish
+}
+
+async function confirmWish(index: number) {
+  const msg = messages.value[index]
+  if (!msg.wish) return
+
+  if (!userStore.isLoggedIn) {
+    showToast('请先登录再许愿')
+    showChat.value = false
+    return
+  }
+
+  // 检查是否在许愿范围内
+  if (props.isInRange === false) {
+    const distText = props.distance
+      ? (props.distance >= 1000 ? (props.distance / 1000).toFixed(1) + 'km' : props.distance + 'm')
+      : '太远'
+    delete msg.wish
+    messages.value.push({
+      role: 'ai',
+      content: `抱歉哦~ 你当前距离许愿树还有 ${distText},超出了许愿范围。请靠近许愿树后再来找我许愿吧!📿`,
+    })
+    scrollToBottom()
+    return
+  }
+
+  msg.wishSubmitting = true
+  try {
+    await submitWish({
+      treeId: props.treeId || 0,
+      content: msg.wish.content,
+      images: [],
+      lng: locationStore.lng || 0,
+      lat: locationStore.lat || 0,
+      address: props.treeAddress || '',
+      isPublic: true,
+      tags: msg.wish.tags,
+      type: 2,
+    })
+    showSuccessToast('愿望已挂在树上!祝你心想事成 ✨')
+    delete msg.wish
+  } catch {
+    showToast('许愿失败,请稍后再试')
+  } finally {
+    msg.wishSubmitting = false
+  }
+}
+
+watch(showChat, (val) => {
+  if (val) {
+    if (!historyLoaded.value) {
+      loadHistory()
+    }
+    scrollToBottom()
+  }
+})
+</script>
+
+<style scoped>
+.chat-fab {
+  position: fixed;
+  right: 16px;
+  bottom: 80px;
+  width: 48px;
+  height: 48px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #07c160, #05a650);
+  box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+  cursor: pointer;
+  transition: transform 0.2s;
+}
+.chat-fab:active {
+  transform: scale(0.9);
+}
+
+.chat-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #f7f8fa;
+}
+
+.chat-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 14px 16px;
+  background: #fff;
+  border-bottom: 1px solid #ebedf0;
+  flex-shrink: 0;
+}
+.chat-title {
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.chat-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 12px 14px;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.load-more {
+  text-align: center;
+  color: #999;
+  font-size: 12px;
+  padding: 8px;
+  cursor: pointer;
+}
+.chat-empty {
+  text-align: center;
+  color: #999;
+  margin-top: 40px;
+  font-size: 14px;
+  line-height: 2;
+}
+
+.chat-msg {
+  display: flex;
+  max-width: 80%;
+}
+.chat-msg.user {
+  align-self: flex-end;
+}
+.chat-msg.ai {
+  align-self: flex-start;
+}
+.chat-msg.has-wish {
+  width: 100%;
+  max-width: 100%;
+  align-self: stretch;
+  flex-direction: column;
+}
+.chat-msg.has-wish .chat-bubble {
+  width: 100%;
+}
+
+.chat-bubble {
+  padding: 10px 14px;
+  border-radius: 16px;
+  font-size: 14px;
+  line-height: 1.6;
+  word-break: break-word;
+}
+.chat-msg.user .chat-bubble {
+  background: #07c160;
+  color: #fff;
+  border-bottom-right-radius: 4px;
+}
+.chat-msg.ai .chat-bubble {
+  background: #fff;
+  color: #333;
+  border-bottom-left-radius: 4px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
+}
+.chat-bubble.typing {
+  color: #999;
+  padding: 10px 18px;
+}
+.chat-bubble-image {
+  padding: 4px !important;
+  background: transparent !important;
+  box-shadow: none !important;
+}
+
+.chat-footer {
+  display: flex;
+  align-items: flex-end;
+  padding: 8px 12px;
+  background: #fff;
+  border-top: 1px solid #ebedf0;
+  gap: 8px;
+  flex-shrink: 0;
+}
+.chat-footer :deep(.van-cell) {
+  flex: 1;
+  background: #f5f5f5;
+  border-radius: 12px;
+  padding: 8px 14px;
+}
+.chat-footer :deep(.van-field__control) {
+  max-height: 120px;
+  overflow-y: auto;
+}
+
+/* 许愿建议卡 */
+.wish-card {
+  margin: 8px 0 0 0;
+  background: #fff;
+  border-radius: 12px;
+  padding: 16px;
+  border: 1px solid #07c160;
+  box-shadow: 0 2px 8px rgba(7, 193, 96, 0.15);
+}
+.wish-card-header {
+  font-size: 13px;
+  font-weight: 600;
+  color: #07c160;
+  margin-bottom: 8px;
+}
+.wish-card-content {
+  font-size: 14px;
+  line-height: 1.6;
+  color: #333;
+  margin-bottom: 8px;
+}
+.wish-card-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  margin-bottom: 10px;
+}
+.wish-card-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+}
+</style>

+ 23 - 15
wishing-tree-h5/src/components/ImageUploader.vue

@@ -1,20 +1,21 @@
 <template>
   <div class="image-uploader">
-    <!-- 持久化隐藏 input,避免移动端动态创建 input 重复点击失效 -->
+    <!-- 持久化隐藏 input,用 key 强制重建,解决移动端相机二次拍摄失效 -->
     <input
+      :key="'camera-' + cameraKey"
       ref="cameraInput"
       type="file"
       accept="image/*"
       capture="environment"
       style="display: none"
-      @change="onFileChange"
+      @change="onCameraChange"
     />
     <input
       ref="albumInput"
       type="file"
       accept="image/*"
       style="display: none"
-      @change="onFileChange"
+      @change="onAlbumChange"
     />
 
     <div class="upload-grid">
@@ -66,8 +67,7 @@ const showAction = ref(false)
 const uploading = ref(false)
 const cameraInput = ref<HTMLInputElement | null>(null)
 const albumInput = ref<HTMLInputElement | null>(null)
-// 记录本次选择的模式,在 onFileChange 中区分
-let currentMode: 'camera' | 'album' = 'album'
+const cameraKey = ref(0)
 
 const actions = [
   { name: 'camera', description: '拍摄照片' },
@@ -86,12 +86,9 @@ function onSelect(action: { name: string }) {
   showAction.value = false
   if (uploading.value) return
 
-  currentMode = action.name as 'camera' | 'album'
-
   if (action.name === 'camera') {
     cameraInput.value?.click()
   } else {
-    // 设置多选
     const input = albumInput.value
     if (input) {
       input.multiple = props.max - props.modelValue.length > 1
@@ -100,21 +97,34 @@ function onSelect(action: { name: string }) {
   }
 }
 
-async function onFileChange(e: Event) {
+async function onCameraChange(e: Event) {
   const input = e.target as HTMLInputElement
   const files = input.files
   if (!files?.length) {
-    // 用户取消,重置 input 以便下次复用
     input.value = ''
     return
   }
+  await uploadFiles(Array.from(files))
+  // 移动端相机 input 需要重建 DOM 才能再次触发
+  cameraKey.value++
+}
 
+async function onAlbumChange(e: Event) {
+  const input = e.target as HTMLInputElement
+  const files = input.files
+  if (!files?.length) {
+    input.value = ''
+    return
+  }
+  await uploadFiles(Array.from(files))
+  input.value = ''
+}
+
+async function uploadFiles(files: File[]) {
   uploading.value = true
   try {
     showToast('上传中...')
-    const compressed = await Promise.all(
-      Array.from(files).map((f) => compressImage(f))
-    )
+    const compressed = await Promise.all(files.map(compressImage))
     const urls = await Promise.all(compressed.map(uploadImage))
     emit('update:modelValue', [...props.modelValue, ...urls])
   } catch (err: any) {
@@ -123,8 +133,6 @@ async function onFileChange(e: Event) {
     }
   } finally {
     uploading.value = false
-    // 重置 input,否则重复选同一文件不会触发 change
-    input.value = ''
   }
 }
 </script>

+ 7 - 2
wishing-tree-h5/src/stores/user.ts

@@ -7,18 +7,21 @@ export const useUserStore = defineStore('user', () => {
   const nickname = ref(getStorage('nickname') || '游客')
   const phone = ref(getStorage('phone') || '')
   const avatarUrl = ref(getStorage('avatarUrl') || '')
+  const isAdmin = ref(getStorage('isAdmin') === '1')
 
   const isLoggedIn = computed(() => !!token.value)
 
-  function login(userData: { token: string; nickname: string; phone: string; avatarUrl?: string }) {
+  function login(userData: { token: string; nickname: string; phone: string; avatarUrl?: string; isAdmin?: number }) {
     token.value = userData.token
     nickname.value = userData.nickname
     phone.value = userData.phone
     avatarUrl.value = userData.avatarUrl || ''
+    isAdmin.value = userData.isAdmin === 1
     setStorage('token', userData.token)
     setStorage('nickname', userData.nickname)
     setStorage('phone', userData.phone)
     setStorage('avatarUrl', userData.avatarUrl || '')
+    setStorage('isAdmin', String(userData.isAdmin || 0))
   }
 
   function logout() {
@@ -26,11 +29,13 @@ export const useUserStore = defineStore('user', () => {
     nickname.value = '游客'
     phone.value = ''
     avatarUrl.value = ''
+    isAdmin.value = false
     removeStorage('token')
     removeStorage('nickname')
     removeStorage('phone')
     removeStorage('avatarUrl')
+    removeStorage('isAdmin')
   }
 
-  return { token, nickname, phone, avatarUrl, isLoggedIn, login, logout }
+  return { token, nickname, phone, avatarUrl, isAdmin, isLoggedIn, login, logout }
 })

+ 1 - 0
wishing-tree-h5/src/views/MakeWishView.vue

@@ -112,6 +112,7 @@ async function submitWish() {
       address: tree.value.address,
       isPublic: isPublic.value,
       tags: tags.value,
+      type: 1,
     })
     showSuccessToast('愿望已挂在树上!祝你心想事成 ✨')
     setTimeout(() => router.replace('/my-wishes'), 2200)

+ 114 - 1
wishing-tree-h5/src/views/MapView.vue

@@ -3,6 +3,16 @@
     <div ref="mapContainer" class="map-container" />
     <!-- 定位按钮 -->
     <div class="map-controls">
+      <van-button
+        v-if="userStore.isAdmin && pendingCreateLngLat"
+        icon="plus"
+        round
+        type="success"
+        size="small"
+        @click="goCreateTree"
+      >
+        在此创建许愿树
+      </van-button>
       <van-button
         icon="aim"
         round
@@ -67,6 +77,32 @@
         </div>
       </div>
     </van-popup>
+
+    <!-- 管理员创建许愿树弹窗 -->
+    <van-popup
+      v-model:show="showCreatePopup"
+      position="bottom"
+      round
+      :style="{ maxHeight: '70%' }"
+      safe-area-inset-bottom
+    >
+      <div class="create-popup">
+        <h3>创建许愿树</h3>
+        <van-field v-model="createForm.name" label="名称" placeholder="输入许愿树名称" required />
+        <div class="create-cover">
+          <span class="create-cover-label">封面图</span>
+          <ImageUploader v-model="createForm.coverImages" :max="1" />
+        </div>
+        <van-field v-model.number="createForm.radius" label="可许愿范围" placeholder="100" type="number">
+          <template #suffix><span style="color:#999;font-size:14px">米</span></template>
+        </van-field>
+        <van-field :model-value="createForm.lng + ', ' + createForm.lat" label="坐标" readonly />
+        <div class="create-actions">
+          <van-button round plain @click="showCreatePopup = false">取消</van-button>
+          <van-button round type="primary" :loading="creating" @click="handleCreateTree">创建</van-button>
+        </div>
+      </div>
+    </van-popup>
   </div>
 </template>
 <script setup lang="ts">
@@ -76,17 +112,25 @@ import { showToast } from "vant";
 import { useLocationStore } from "@/stores/location";
 import { getUserLocation } from "@/utils/geo";
 import { getTreeGradient, getTreeEmoji } from "@/utils/theme";
-import { fetchNearbyTrees } from "@/api/tree";
+import { fetchNearbyTrees, createTree } from "@/api/tree";
+import { useUserStore } from "@/stores/user";
 import { gcj02ToWgs84 } from "@/utils/mapTool";
 import markerImg from "@/assets/location-marker.png";
 import L from "leaflet";
 import "leaflet/dist/leaflet.css";
+import ImageUploader from "@/components/ImageUploader.vue";
 const router = useRouter();
 const locationStore = useLocationStore();
+const userStore = useUserStore();
 const mapContainer = ref<HTMLDivElement>();
 const showPopup = ref(false);
 const selectedTree = ref<any>(null);
 const locating = ref(false);
+
+// 管理员创建许愿树
+const showCreatePopup = ref(false);
+const creating = ref(false);
+const createForm = ref({ name: '', radius: 100, lng: 0, lat: 0, coverImages: [] as string[] });
 const popupGradient = computed(() =>
   getTreeGradient(selectedTree.value?.id || 1),
 );
@@ -94,6 +138,7 @@ const popupEmoji = computed(() => getTreeEmoji(selectedTree.value?.id || 1));
 let map: any = null;
 let userMarker: any = null;
 let treeMarkers: L.Marker[] = [];
+const pendingCreateLngLat = ref<[number, number] | null>(null);
 function fmtDist(m: number) {
   return m >= 1000 ? (m / 1000).toFixed(1) + "km" : m + "m";
 }
@@ -105,6 +150,37 @@ function goMakeWish() {
   showPopup.value = false;
   router.push({ path: "/make-wish", query: { treeId: selectedTree.value.id } });
 }
+function goCreateTree() {
+  if (!pendingCreateLngLat.value) return;
+  const [lng, lat] = pendingCreateLngLat.value;
+  createForm.value = { name: '', radius: 100, lng: Number(lng.toFixed(6)), lat: Number(lat.toFixed(6)), coverImages: [] };
+  showCreatePopup.value = true;
+}
+async function handleCreateTree() {
+  if (!createForm.value.name.trim()) {
+    showToast("请输入许愿树名称");
+    return;
+  }
+  creating.value = true;
+  try {
+    await createTree({
+      name: createForm.value.name,
+      description: '',
+      longitude: createForm.value.lng,
+      latitude: createForm.value.lat,
+      address: '',
+      radius: createForm.value.radius,
+      coverImage: createForm.value.coverImages[0] || '',
+    });
+    showToast("许愿树创建成功");
+    showCreatePopup.value = false;
+    await loadTrees();
+  } catch {
+    showToast("创建失败");
+  } finally {
+    creating.value = false;
+  }
+}
 // 高德瓦片(GCJ-02 坐标系)
 async function initMap() {
   try {
@@ -154,6 +230,20 @@ async function initMap() {
     L.layerGroup().addTo(map);
     L.control.scale({ metric: true, imperial: false }).addTo(map);
     L.control.zoom({ position: "topright" }).addTo(map);
+    // 管理员点击地图:移动定位标记到点击位置
+    map.on("click", (e: any) => {
+      if (!userStore.isAdmin) return;
+      showPopup.value = false;
+      const { lat, lng } = e.latlng;
+      pendingCreateLngLat.value = [lng, lat];
+      if (userMarker) map.removeLayer(userMarker);
+      const icon = L.icon({
+        iconUrl: markerImg,
+        iconSize: [25, 45],
+        iconAnchor: [12, 45],
+      });
+      userMarker = L.marker([lat, lng], { icon, zIndexOffset: 200 }).addTo(map);
+    });
     locateUser();
   } catch (err) {
     showToast("地图加载失败");
@@ -315,4 +405,27 @@ onUnmounted(() => {
   display: flex;
   gap: 8px;
 }
+.create-popup {
+  padding: 16px;
+}
+.create-popup h3 {
+  font-size: 18px;
+  margin-bottom: 12px;
+  text-align: center;
+}
+.create-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+  padding: 16px 0;
+}
+.create-cover {
+  padding: 12px 16px;
+}
+.create-cover-label {
+  font-size: 14px;
+  color: #666;
+  margin-bottom: 8px;
+  display: block;
+}
 </style>

+ 3 - 0
wishing-tree-h5/src/views/TreeDetailView.vue

@@ -47,6 +47,8 @@
     </div>
 
     <van-loading v-else class="loading" />
+
+    <ChatWidget :tree-id="tree?.id" :tree-name="tree?.name" :tree-address="tree?.address" :is-in-range="tree?.isInRange" :distance="tree?.distance" />
   </div>
 </template>
 
@@ -57,6 +59,7 @@ import { useLocationStore } from '@/stores/location'
 import { fetchTreeDetail } from '@/api/tree'
 import { fetchTreeWishes } from '@/api/wish'
 import WishCard from '@/components/WishCard.vue'
+import ChatWidget from '@/components/ChatWidget.vue'
 import { getTreeGradient, getTreeEmoji } from '@/utils/theme'
 import type { WishingTree } from '@/mock/data'
 import type { Wish } from '@/mock/data'

BIN
wishing-tree-h5/wish.zip


BIN
许愿树创业计划书.pptx