wangwenju 1 сар өмнө
parent
commit
2547ae4ad7

+ 214 - 0
LiangZhiYUMao/components/common-tabbar/common-tabbar.vue

@@ -0,0 +1,214 @@
+<template>
+  <view class="tabbar" v-if="tabbarList.length > 0">
+    <view 
+      class="tabbar-item" 
+      :class="{ active: currentTab === item.tabKey }"
+      v-for="item in tabbarList" 
+      :key="item.id"
+      @click="switchTab(item)"
+    >
+      <text class="tabbar-icon">{{ currentTab === item.tabKey ? (item.iconSelected || item.icon) : item.icon }}</text>
+      <text class="tabbar-text">{{ item.name }}</text>
+      <!-- 角标 -->
+      <view v-if="item.badgeType === 'dot'" class="tabbar-badge-dot"></view>
+      <view v-else-if="item.badgeType === 'number' && getBadgeValue(item.badgeKey) > 0" class="tabbar-badge">
+        {{ getBadgeValue(item.badgeKey) > 99 ? '99+' : getBadgeValue(item.badgeKey) }}
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+  name: 'CommonTabbar',
+  props: {
+    // 当前选中的tab
+    current: {
+      type: String,
+      default: ''
+    },
+    // 客户端类型:user-用户端,matchmaker-红娘端
+    clientType: {
+      type: String,
+      default: 'user'
+    },
+    // 角标数据(key-value形式)
+    badgeData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  data() {
+    return {
+      tabbarList: [],
+      currentTab: '',
+      defaultTabbar: [
+        { id: 1, name: '首页', icon: '🏠', iconSelected: '🏠', path: '/pages/index/index', tabKey: 'index', sortOrder: 1 },
+        { id: 2, name: '动态', icon: '💕', iconSelected: '💕', path: '/pages/plaza/index', tabKey: 'plaza', sortOrder: 2 },
+        { id: 3, name: '推荐', icon: '👍', iconSelected: '👍', path: '/pages/recommend/index', tabKey: 'recommend', sortOrder: 3 },
+        { id: 4, name: '我的', icon: '👤', iconSelected: '👤', path: '/pages/mine/index', tabKey: 'mine', sortOrder: 4 }
+      ]
+    }
+  },
+  watch: {
+    current: {
+      immediate: true,
+      handler(val) {
+        this.currentTab = val
+      }
+    }
+  },
+  created() {
+    this.loadTabbarConfig()
+  },
+  methods: {
+    // 加载导航配置
+    async loadTabbarConfig() {
+      try {
+        // 先尝试从缓存获取
+        const cacheKey = `tabbar_config_${this.clientType}`
+        const cached = uni.getStorageSync(cacheKey)
+        const cacheTime = uni.getStorageSync(cacheKey + '_time')
+        
+        // 缓存有效期5分钟
+        if (cached && cacheTime && (Date.now() - cacheTime < 5 * 60 * 1000)) {
+          this.tabbarList = cached
+          return
+        }
+        
+        // 从服务器获取
+        const list = await api.tabbar.getTabbarConfig(this.clientType)
+        if (list && list.length > 0) {
+          this.tabbarList = list
+          // 缓存配置
+          uni.setStorageSync(cacheKey, list)
+          uni.setStorageSync(cacheKey + '_time', Date.now())
+        } else {
+          // 使用默认配置
+          this.tabbarList = this.defaultTabbar
+        }
+      } catch (e) {
+        console.error('加载导航配置失败:', e)
+        // 使用默认配置
+        this.tabbarList = this.defaultTabbar
+      }
+    },
+    
+    // 切换tab
+    switchTab(item) {
+      if (this.currentTab === item.tabKey) {
+        return
+      }
+      
+      this.currentTab = item.tabKey
+      this.$emit('change', item)
+      
+      // 跳转页面
+      uni.switchTab({
+        url: item.path,
+        fail: () => {
+          // switchTab失败时尝试navigateTo
+          uni.navigateTo({
+            url: item.path,
+            fail: () => {
+              uni.redirectTo({
+                url: item.path
+              })
+            }
+          })
+        }
+      })
+    },
+    
+    // 获取角标值
+    getBadgeValue(key) {
+      if (!key) return 0
+      return this.badgeData[key] || 0
+    },
+    
+    // 刷新配置(供外部调用)
+    refreshConfig() {
+      const cacheKey = `tabbar_config_${this.clientType}`
+      uni.removeStorageSync(cacheKey)
+      uni.removeStorageSync(cacheKey + '_time')
+      this.loadTabbarConfig()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.tabbar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 100rpx;
+  background: #FFFFFF;
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
+  z-index: 999;
+  
+  .tabbar-item {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    padding: 10rpx 0;
+    
+    .tabbar-icon {
+      font-size: 44rpx;
+      margin-bottom: 4rpx;
+    }
+    
+    .tabbar-text {
+      font-size: 22rpx;
+      color: #999999;
+    }
+    
+    &.active {
+      .tabbar-text {
+        color: #E91E63;
+        font-weight: 500;
+      }
+    }
+    
+    // 数字角标
+    .tabbar-badge {
+      position: absolute;
+      top: 2rpx;
+      right: 50%;
+      transform: translateX(30rpx);
+      min-width: 32rpx;
+      height: 32rpx;
+      line-height: 32rpx;
+      padding: 0 8rpx;
+      background: #FF4757;
+      color: #FFFFFF;
+      font-size: 20rpx;
+      border-radius: 16rpx;
+      text-align: center;
+    }
+    
+    // 红点角标
+    .tabbar-badge-dot {
+      position: absolute;
+      top: 8rpx;
+      right: 50%;
+      transform: translateX(24rpx);
+      width: 16rpx;
+      height: 16rpx;
+      background: #FF4757;
+      border-radius: 50%;
+    }
+  }
+}
+</style>

+ 10 - 23
LiangZhiYUMao/pages/index/index.vue

@@ -174,29 +174,7 @@
 		<view class="bottom-placeholder"></view>
 
 		<!-- 底部导航栏 -->
-				<view class="tabbar">
-					<view class="tabbar-item active" @click="switchTab('index')">
-						<text class="tabbar-icon">🏠</text>
-						<text class="tabbar-text">首页</text>
-					</view>
-					<view class="tabbar-item" @click="switchTab('plaza')">
-						<text class="tabbar-icon">💕</text>
-						<text class="tabbar-text">动态</text>
-					</view>
-					<view class="tabbar-item" @click="switchTab('recommend')">
-						<text class="tabbar-icon">👍</text>
-						<text class="tabbar-text">推荐</text>
-					</view>
-					<!-- <view class="tabbar-item" @click="switchTab('message')">
-						<text class="tabbar-icon">💬</text>
-						<text class="tabbar-text">消息</text>
-						<view v-if="unreadCount >= 1" class="tabbar-badge">{{ unreadCount }}</view>
-					</view> -->
-					<view class="tabbar-item" @click="switchTab('mine')">
-						<text class="tabbar-icon">👤</text>
-						<text class="tabbar-text">我的</text>
-					</view>
-				</view>
+		<common-tabbar current="index" client-type="user" :badge-data="badgeData" />
 
 		<!-- 红娘提示弹框 -->
 		<uni-popup ref="matchmakerPopup" type="dialog">
@@ -219,14 +197,23 @@
 	import { DEFAULT_IMAGES, ACTIVITY_TYPES } from '@/config/index.js'
 	
 	import timManager from '@/utils/tim-manager.js'
+	import CommonTabbar from '@/components/common-tabbar/common-tabbar.vue'
 
 	export default {
+		components: {
+			CommonTabbar
+		},
 		data() {
 			return {
 				// 用户信息
 				userInfo: {},
 				matchCount: 3,
 				
+				// 角标数据
+				badgeData: {
+					unreadCount: 0
+				},
+				
 				// 是否显示红娘工作台按钮(根据数据库查询结果)
 				showMatchmakerButton: false,
 

+ 8 - 23
LiangZhiYUMao/pages/mine/index.vue

@@ -233,41 +233,26 @@
     </view>
 
    <!-- 底部导航栏 -->
-   		<view class="tabbar">
-    <view class="tabbar-item" @click="switchTab('index')">
-    <text class="tabbar-icon">🏠</text>
-    <text class="tabbar-text">首页</text>
-    </view>
-    <view class="tabbar-item" @click="switchTab('plaza')">
-    <text class="tabbar-icon">💕</text>
-    <text class="tabbar-text">动态</text>
-    </view>
-    <view class="tabbar-item" @click="switchTab('recommend')">
-    <text class="tabbar-icon">👍</text>
-    <text class="tabbar-text">推荐</text>
-    </view>
-    <!-- <view class="tabbar-item" @click="switchTab('message')">
-    <text class="tabbar-icon">💬</text>
-    <text class="tabbar-text">消息</text>
-    <view v-if="unreadCount >= 1" class="tabbar-badge">{{ unreadCount }}</view>
-    </view> -->
-    <view class="tabbar-item active" @click="switchTab('mine')">
-    <text class="tabbar-icon">👤</text>
-    <text class="tabbar-text">我的</text>
-    </view>
-    </view>
+   <common-tabbar current="mine" client-type="user" :badge-data="badgeData" />
   </view>
 </template>
 
 <script>
 import api from '@/utils/api.js'
 import userStatusCheck from '@/utils/userStatusCheck.js'
+import CommonTabbar from '@/components/common-tabbar/common-tabbar.vue'
 
 export default {
+  components: {
+    CommonTabbar
+  },
   data() {
     return {
       // 网关地址
       gatewayURL: 'https://api.zhongruanke.cn',
+      
+      // 角标数据
+      badgeData: { unreadCount: 0 },
 
       // 用户ID(如果没有登录,默认为1)
       currentUserId: null,

+ 2 - 2
LiangZhiYUMao/pages/page3/page3.vue

@@ -17,8 +17,8 @@
       <!-- 微信一键登录 -->
       <button class="wechat-login-btn" @click="login_zheshow">
 		 
-        <image class="wechat-icon" src="https://api.zhongruanke.cn/minio/static-images/wechat-icon.png" mode="aspectFit"></image>
-        <text>微信一键登录</text>
+        <!-- <image class="wechat-icon" src="https://api.zhongruanke.cn/minio/static-images/wechat-icon.png" mode="aspectFit"></image> -->
+        <text>一键登录</text>
       </button>
 
     </view>

+ 7 - 24
LiangZhiYUMao/pages/plaza/index.vue

@@ -139,37 +139,19 @@
 		</view>
 		
 		<!-- 底部导航栏 -->
-		<view class="tabbar">
-			<view class="tabbar-item" @click="switchTab('index')">
-				<text class="tabbar-icon">🏠</text>
-				<text class="tabbar-text">首页</text>
-			</view>
-			<view class="tabbar-item active" @click="switchTab('plaza')">
-				<text class="tabbar-icon">💕</text>
-				<text class="tabbar-text">动态</text>
-			</view>
-			<view class="tabbar-item" @click="switchTab('recommend')">
-				<text class="tabbar-icon">👍</text>
-				<text class="tabbar-text">推荐</text>
-			</view>
-			<!-- <view class="tabbar-item" @click="switchTab('message')">
-				<text class="tabbar-icon">💬</text>
-				<text class="tabbar-text">消息</text>
-				<view v-if="unreadCount > 0" class="tabbar-badge">{{ unreadCount }}</view>
-			</view> -->
-			<view class="tabbar-item" @click="switchTab('mine')">
-				<text class="tabbar-icon">👤</text>
-				<text class="tabbar-text">我的</text>
-			</view>
-		</view>
+		<common-tabbar current="plaza" client-type="user" :badge-data="badgeData" />
 	</view>
 </template>
 
 <script>
 import api from '@/utils/api.js'
 import userStatusCheck from '@/utils/userStatusCheck.js'
+import CommonTabbar from '@/components/common-tabbar/common-tabbar.vue'
 
 export default {
+	components: {
+		CommonTabbar
+	},
 	data() {
 		return {
 			dynamicList: [],
@@ -186,7 +168,8 @@ export default {
 			favoritingMap: {},  // 记录正在收藏的动态ID,防止重复点击
 			scrollTopValue: 0, // 用于设置滚动位置
 			savedScrollTop: 0, // 保存的滚动位置
-			isFirstLoad: true // 是否首次加载
+			isFirstLoad: true, // 是否首次加载
+			badgeData: { unreadCount: 0 } // 角标数据
 		}
 	},
 	computed: {

+ 5 - 24
LiangZhiYUMao/pages/recommend/index.vue

@@ -223,29 +223,7 @@
 			</view>
 		</scroll-view>
 		<!-- 底部导航栏 -->
-		<view class="tabbar">
-			<view class="tabbar-item" @click="switchTab('index')">
-				<text class="tabbar-icon">🏠</text>
-				<text class="tabbar-text">首页</text>
-			</view>
-			<view class="tabbar-item" @click="switchTab('plaza')">
-				<text class="tabbar-icon">💕</text>
-				<text class="tabbar-text">动态</text>
-			</view>
-			<view class="tabbar-item active" @click="switchTab('recommend')">
-				<text class="tabbar-icon">👍</text>
-				<text class="tabbar-text">推荐</text>
-			</view>
-			<!-- <view class="tabbar-item" @click="switchTab('message')">
-				<text class="tabbar-icon">💬</text>
-				<text class="tabbar-text">消息</text>
-				<view v-if="unreadCount >= 1" class="tabbar-badge">{{ unreadCount }}</view>
-			</view> -->
-			<view class="tabbar-item" @click="switchTab('mine')">
-				<text class="tabbar-icon">👤</text>
-				<text class="tabbar-text">我的</text>
-			</view>
-		</view>
+		<common-tabbar current="recommend" client-type="user" :badge-data="badgeData" />
 	</view>
 </template>
 
@@ -253,11 +231,14 @@
 import api from '@/utils/api.js'
 import userStatusCheck from '@/utils/userStatusCheck.js'
 import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
+import CommonTabbar from '@/components/common-tabbar/common-tabbar.vue'
+
 export default {
-  components: { uniPopup },
+  components: { uniPopup, CommonTabbar },
 	data() {
 		return { 
 			currentTab: 'recommend', // 当前选中的选项卡
+			badgeData: { unreadCount: 0 }, // 角标数据
 			list: [], 
 			loading: false, 
 			oppoOnly: 1,

+ 9 - 0
LiangZhiYUMao/utils/api.js

@@ -99,6 +99,15 @@ const request = (options) => {
  * API 接口列表
  */
 export default {
+  // 底部导航栏配置
+  tabbar: {
+    // 获取用户端导航配置
+    getUserTabbar: () => request({ url: '/tabbar/user' }),
+    // 获取红娘端导航配置
+    getMatchmakerTabbar: () => request({ url: '/tabbar/matchmaker' }),
+    // 获取指定客户端类型的导航配置
+    getTabbarConfig: (clientType = 'user') => request({ url: `/tabbar/config?clientType=${clientType}` })
+  },
   // 地区
   area: {
     // 获取省份列表

+ 145 - 151
LiangZhiYUMao/utils/contentSecurityCheck.js

@@ -18,190 +18,184 @@ const SENSITIVE_WORDS = [
   '刷单', '兼职日结', '高额返利', '免费领取'
 ]
 
-export default {
-  /**
-   * 检测文字内容是否安全
-   * @param {string} content - 要检测的文字内容
-   * @returns {Promise<{safe: boolean, message: string}>}
-   */
-  async checkText(content) {
-    if (!content || !content.trim()) {
-      return { safe: true, message: '' }
+/**
+ * 本地敏感词检测
+ */
+function localTextCheck(content) {
+  if (!content) return { safe: true, message: '' }
+  
+  const lowerContent = content.toLowerCase()
+  for (const word of SENSITIVE_WORDS) {
+    if (lowerContent.includes(word.toLowerCase())) {
+      return {
+        safe: false,
+        message: '内容包含敏感词,请修改后重试'
+      }
     }
+  }
+  return { safe: true, message: '' }
+}
+
+/**
+ * 检测文字内容是否安全
+ */
+async function checkText(content) {
+  if (!content || !content.trim()) {
+    return { safe: true, message: '' }
+  }
+  
+  // 1. 本地敏感词过滤(快速检测)
+  const localResult = localTextCheck(content)
+  if (!localResult.safe) {
+    return localResult
+  }
+  
+  // 2. 调用后端接口进行内容安全检测
+  try {
+    const [error, res] = await uni.request({
+      url: BASE_URL + '/content-security/check-text',
+      method: 'POST',
+      data: { content: content },
+      timeout: 10000
+    })
     
-    // 1. 本地敏感词过滤(快速检测)
-    const localResult = this.localTextCheck(content)
-    if (!localResult.safe) {
+    if (error) {
+      console.error('文字安全检测请求失败:', error)
       return localResult
     }
     
-    // 2. 调用后端接口进行内容安全检测
-    try {
-      const [error, res] = await uni.request({
-        url: `${BASE_URL}/content-security/check-text`,
-        method: 'POST',
-        data: { content: content },
-        timeout: 10000
-      })
-      
-      if (error) {
-        console.error('文字安全检测请求失败:', error)
-        // 网络错误时,仅依赖本地检测结果
-        return localResult
-      }
-      
-      if (res.statusCode === 200 && res.data) {
-        if (res.data.code === 200) {
-          const data = res.data.data
-          if (data && data.safe === false) {
-            return {
-              safe: false,
-              message: data.message || '内容包含违规信息,请修改后重试'
-            }
-          }
-        } else if (res.data.code === 87014) {
-          // 微信内容安全API返回的违规码
+    if (res.statusCode === 200 && res.data) {
+      if (res.data.code === 200) {
+        const data = res.data.data
+        if (data && data.safe === false) {
           return {
             safe: false,
-            message: '内容包含违规信息,请修改后重试'
+            message: data.message || '内容包含违规信息,请修改后重试'
           }
         }
-      }
-      
-      return { safe: true, message: '' }
-    } catch (e) {
-      console.error('文字安全检测异常:', e)
-      return localResult
-    }
-  },
-  
-  /**
-   * 本地敏感词检测
-   * @param {string} content - 要检测的文字内容
-   * @returns {{safe: boolean, message: string}}
-   */
-  localTextCheck(content) {
-    if (!content) return { safe: true, message: '' }
-    
-    const lowerContent = content.toLowerCase()
-    for (const word of SENSITIVE_WORDS) {
-      if (lowerContent.includes(word.toLowerCase())) {
+      } else if (res.data.code === 87014) {
         return {
           safe: false,
-          message: '内容包含敏感词,请修改后重试'
+          message: '内容包含违规信息,请修改后重试'
         }
       }
     }
+    
     return { safe: true, message: '' }
-  },
+  } catch (e) {
+    console.error('文字安全检测异常:', e)
+    return localResult
+  }
+}
+
+/**
+ * 检测图片是否安全
+ */
+async function checkImage(imageUrl) {
+  if (!imageUrl) {
+    return { safe: true, message: '' }
+  }
   
-  /**
-   * 检测图片是否安全
-   * @param {string} imageUrl - 图片URL(需要是可访问的网络地址)
-   * @returns {Promise<{safe: boolean, message: string}>}
-   */
-  async checkImage(imageUrl) {
-    if (!imageUrl) {
-      return { safe: true, message: '' }
-    }
+  // 如果是本地临时文件,跳过检测
+  if (imageUrl.startsWith('wxfile://') || imageUrl.startsWith('http://tmp/')) {
+    return { safe: true, message: '' }
+  }
+  
+  try {
+    const [error, res] = await uni.request({
+      url: BASE_URL + '/content-security/check-image',
+      method: 'POST',
+      data: { imageUrl: imageUrl },
+      timeout: 15000
+    })
     
-    // 如果是本地临时文件,跳过检测(上传后再检测)
-    if (imageUrl.startsWith('wxfile://') || imageUrl.startsWith('http://tmp/')) {
+    if (error) {
+      console.error('图片安全检测请求失败:', error)
       return { safe: true, message: '' }
     }
     
-    try {
-      const [error, res] = await uni.request({
-        url: `${BASE_URL}/content-security/check-image`,
-        method: 'POST',
-        data: { imageUrl: imageUrl },
-        timeout: 15000
-      })
-      
-      if (error) {
-        console.error('图片安全检测请求失败:', error)
-        return { safe: true, message: '' } // 网络错误时默认通过
-      }
-      
-      if (res.statusCode === 200 && res.data) {
-        if (res.data.code === 200) {
-          const data = res.data.data
-          if (data && data.safe === false) {
-            return {
-              safe: false,
-              message: data.message || '图片包含违规内容,请更换后重试'
-            }
-          }
-        } else if (res.data.code === 87014) {
+    if (res.statusCode === 200 && res.data) {
+      if (res.data.code === 200) {
+        const data = res.data.data
+        if (data && data.safe === false) {
           return {
             safe: false,
-            message: '图片包含违规内容,请更换后重试'
+            message: data.message || '图片包含违规内容,请更换后重试'
           }
         }
-      }
-      
-      return { safe: true, message: '' }
-    } catch (e) {
-      console.error('图片安全检测异常:', e)
-      return { safe: true, message: '' }
-    }
-  },
-  
-  /**
-   * 批量检测图片是否安全
-   * @param {string[]} imageUrls - 图片URL数组
-   * @returns {Promise<{safe: boolean, message: string, failedIndex: number}>}
-   */
-  async checkImages(imageUrls) {
-    if (!imageUrls || imageUrls.length === 0) {
-      return { safe: true, message: '', failedIndex: -1 }
-    }
-    
-    // 过滤掉本地临时文件
-    const networkUrls = imageUrls.filter(url => 
-      url && !url.startsWith('wxfile://') && !url.startsWith('http://tmp/')
-    )
-    
-    if (networkUrls.length === 0) {
-      return { safe: true, message: '', failedIndex: -1 }
-    }
-    
-    // 逐个检测图片
-    for (let i = 0; i < networkUrls.length; i++) {
-      const result = await this.checkImage(networkUrls[i])
-      if (!result.safe) {
+      } else if (res.data.code === 87014) {
         return {
           safe: false,
-          message: `第${i + 1}张图片包含违规内容,请更换后重试`,
-          failedIndex: i
+          message: '图片包含违规内容,请更换后重试'
         }
       }
     }
     
+    return { safe: true, message: '' }
+  } catch (e) {
+    console.error('图片安全检测异常:', e)
+    return { safe: true, message: '' }
+  }
+}
+
+/**
+ * 批量检测图片是否安全
+ */
+async function checkImages(imageUrls) {
+  if (!imageUrls || imageUrls.length === 0) {
     return { safe: true, message: '', failedIndex: -1 }
-  },
+  }
   
-  /**
-   * 综合检测文字和图片
-   * @param {string} content - 文字内容
-   * @param {string[]} imageUrls - 图片URL数组
-   * @returns {Promise<{safe: boolean, message: string}>}
-   */
-  async checkContent(content, imageUrls) {
-    // 1. 检测文字
-    const textResult = await this.checkText(content)
-    if (!textResult.safe) {
-      return textResult
-    }
-    
-    // 2. 检测图片
-    if (imageUrls && imageUrls.length > 0) {
-      const imageResult = await this.checkImages(imageUrls)
-      if (!imageResult.safe) {
-        return imageResult
+  // 过滤掉本地临时文件
+  const networkUrls = imageUrls.filter(url => 
+    url && !url.startsWith('wxfile://') && !url.startsWith('http://tmp/')
+  )
+  
+  if (networkUrls.length === 0) {
+    return { safe: true, message: '', failedIndex: -1 }
+  }
+  
+  // 逐个检测图片
+  for (let i = 0; i < networkUrls.length; i++) {
+    const result = await checkImage(networkUrls[i])
+    if (!result.safe) {
+      return {
+        safe: false,
+        message: `第${i + 1}张图片包含违规内容,请更换后重试`,
+        failedIndex: i
       }
     }
-    
-    return { safe: true, message: '' }
   }
+  
+  return { safe: true, message: '', failedIndex: -1 }
+}
+
+/**
+ * 综合检测文字和图片
+ */
+async function checkContent(content, imageUrls) {
+  // 1. 检测文字
+  const textResult = await checkText(content)
+  if (!textResult.safe) {
+    return textResult
+  }
+  
+  // 2. 检测图片
+  if (imageUrls && imageUrls.length > 0) {
+    const imageResult = await checkImages(imageUrls)
+    if (!imageResult.safe) {
+      return imageResult
+    }
+  }
+  
+  return { safe: true, message: '' }
+}
+
+// 导出模块
+export default {
+  checkText,
+  checkImage,
+  checkImages,
+  checkContent,
+  localTextCheck
 }

+ 2 - 2
gateway/src/main/resources/application.yml

@@ -112,7 +112,7 @@ spring:
         - id: content-security-route
           uri: http://localhost:8086
           predicates:
-            - Path=/content-security/**
+            - Path=/api/content-security/**
           filters:
             - StripPrefix=0
 
@@ -144,7 +144,7 @@ spring:
         - id: essential-route
           uri: http://localhost:1005
           predicates:
-            - Path=/api/user/**, /api/checkin/**, /api/vip/**, /api/activity-order/**, /api/activity-registration/**, /api/avatar/**
+            - Path=/api/user/**, /api/checkin/**, /api/vip/**, /api/activity-order/**, /api/activity-registration/**, /api/avatar/**, /api/tabbar/**
           filters:
             - StripPrefix=0
         

+ 12 - 2
pom.xml

@@ -76,11 +76,21 @@
 
 
 
-      <!-- JSON processing -->
+      <!-- JSON processing - 统一Jackson版本,与Spring Boot 2.7.15兼容 -->
+      <dependency>
+        <groupId>com.fasterxml.jackson.core</groupId>
+        <artifactId>jackson-core</artifactId>
+        <version>2.13.5</version>
+      </dependency>
       <dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-databind</artifactId>
-        <version>2.15.2</version>
+        <version>2.13.5</version>
+      </dependency>
+      <dependency>
+        <groupId>com.fasterxml.jackson.core</groupId>
+        <artifactId>jackson-annotations</artifactId>
+        <version>2.13.5</version>
       </dependency>
 
       <!-- Lombok -->

+ 92 - 0
service/Essential/src/main/java/com/zhentao/controller/TabbarConfigController.java

@@ -0,0 +1,92 @@
+package com.zhentao.controller;
+
+import com.zhentao.common.Result;
+import com.zhentao.entity.TabbarConfig;
+import com.zhentao.service.TabbarConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 底部导航栏配置控制器
+ */
+@RestController
+@RequestMapping("/api/tabbar")
+public class TabbarConfigController {
+    
+    @Autowired
+    private TabbarConfigService tabbarConfigService;
+    
+    /**
+     * 获取用户端导航配置
+     */
+    @GetMapping("/user")
+    public Result<List<TabbarConfig>> getUserTabbar() {
+        List<TabbarConfig> list = tabbarConfigService.getUserTabbarConfig();
+        return Result.success(list);
+    }
+    
+    /**
+     * 获取红娘端导航配置
+     */
+    @GetMapping("/matchmaker")
+    public Result<List<TabbarConfig>> getMatchmakerTabbar() {
+        List<TabbarConfig> list = tabbarConfigService.getMatchmakerTabbarConfig();
+        return Result.success(list);
+    }
+    
+    /**
+     * 获取指定客户端类型的导航配置
+     */
+    @GetMapping("/config")
+    public Result<List<TabbarConfig>> getTabbarConfig(@RequestParam(defaultValue = "user") String clientType) {
+        List<TabbarConfig> list = tabbarConfigService.getTabbarConfig(clientType);
+        return Result.success(list);
+    }
+    
+    /**
+     * 获取所有导航配置(管理端使用)
+     */
+    @GetMapping("/all")
+    public Result<List<TabbarConfig>> getAllTabbarConfig(@RequestParam(defaultValue = "user") String clientType) {
+        List<TabbarConfig> list = tabbarConfigService.getAllTabbarConfig(clientType);
+        return Result.success(list);
+    }
+    
+    /**
+     * 更新导航配置
+     */
+    @PutMapping("/update")
+    public Result<Boolean> updateTabbarConfig(@RequestBody TabbarConfig config) {
+        boolean success = tabbarConfigService.updateTabbarConfig(config);
+        return success ? Result.success(true) : Result.error("更新失败");
+    }
+    
+    /**
+     * 添加导航配置
+     */
+    @PostMapping("/add")
+    public Result<Boolean> addTabbarConfig(@RequestBody TabbarConfig config) {
+        boolean success = tabbarConfigService.addTabbarConfig(config);
+        return success ? Result.success(true) : Result.error("添加失败");
+    }
+    
+    /**
+     * 删除导航配置
+     */
+    @DeleteMapping("/delete/{id}")
+    public Result<Boolean> deleteTabbarConfig(@PathVariable Long id) {
+        boolean success = tabbarConfigService.deleteTabbarConfig(id);
+        return success ? Result.success(true) : Result.error("删除失败");
+    }
+    
+    /**
+     * 启用/禁用导航项
+     */
+    @PostMapping("/toggle/{id}")
+    public Result<Boolean> toggleTabbarItem(@PathVariable Long id, @RequestParam Boolean enabled) {
+        boolean success = tabbarConfigService.toggleTabbarItem(id, enabled);
+        return success ? Result.success(true) : Result.error("操作失败");
+    }
+}

+ 73 - 0
service/Essential/src/main/java/com/zhentao/entity/TabbarConfig.java

@@ -0,0 +1,73 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 底部导航栏配置实体
+ */
+@Data
+@TableName("tabbar_config")
+public class TabbarConfig {
+    
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    
+    /**
+     * 导航项名称
+     */
+    private String name;
+    
+    /**
+     * 图标(emoji或图片URL)
+     */
+    private String icon;
+    
+    /**
+     * 选中状态图标
+     */
+    private String iconSelected;
+    
+    /**
+     * 跳转路径
+     */
+    private String path;
+    
+    /**
+     * 导航项标识
+     */
+    private String tabKey;
+    
+    /**
+     * 排序顺序
+     */
+    private Integer sortOrder;
+    
+    /**
+     * 是否启用
+     */
+    private Boolean isEnabled;
+    
+    /**
+     * 角标类型:none-无,dot-红点,number-数字
+     */
+    private String badgeType;
+    
+    /**
+     * 角标数据key
+     */
+    private String badgeKey;
+    
+    /**
+     * 客户端类型:user-用户端,matchmaker-红娘端
+     */
+    private String clientType;
+    
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+    
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+}

+ 28 - 0
service/Essential/src/main/java/com/zhentao/mapper/TabbarConfigMapper.java

@@ -0,0 +1,28 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.TabbarConfig;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 底部导航栏配置Mapper
+ */
+@Mapper
+public interface TabbarConfigMapper extends BaseMapper<TabbarConfig> {
+    
+    /**
+     * 获取指定客户端类型的启用导航配置
+     */
+    @Select("SELECT * FROM tabbar_config WHERE client_type = #{clientType} AND is_enabled = 1 ORDER BY sort_order ASC")
+    List<TabbarConfig> selectEnabledByClientType(@Param("clientType") String clientType);
+    
+    /**
+     * 获取所有导航配置(包括禁用的)
+     */
+    @Select("SELECT * FROM tabbar_config WHERE client_type = #{clientType} ORDER BY sort_order ASC")
+    List<TabbarConfig> selectAllByClientType(@Param("clientType") String clientType);
+}

+ 51 - 0
service/Essential/src/main/java/com/zhentao/service/TabbarConfigService.java

@@ -0,0 +1,51 @@
+package com.zhentao.service;
+
+import com.zhentao.entity.TabbarConfig;
+
+import java.util.List;
+
+/**
+ * 底部导航栏配置服务接口
+ */
+public interface TabbarConfigService {
+    
+    /**
+     * 获取用户端导航配置
+     */
+    List<TabbarConfig> getUserTabbarConfig();
+    
+    /**
+     * 获取红娘端导航配置
+     */
+    List<TabbarConfig> getMatchmakerTabbarConfig();
+    
+    /**
+     * 获取指定客户端类型的导航配置
+     */
+    List<TabbarConfig> getTabbarConfig(String clientType);
+    
+    /**
+     * 获取所有导航配置(管理端使用)
+     */
+    List<TabbarConfig> getAllTabbarConfig(String clientType);
+    
+    /**
+     * 更新导航配置
+     */
+    boolean updateTabbarConfig(TabbarConfig config);
+    
+    /**
+     * 添加导航配置
+     */
+    boolean addTabbarConfig(TabbarConfig config);
+    
+    /**
+     * 删除导航配置
+     */
+    boolean deleteTabbarConfig(Long id);
+    
+    /**
+     * 启用/禁用导航项
+     */
+    boolean toggleTabbarItem(Long id, Boolean enabled);
+}

+ 65 - 17
service/Essential/src/main/java/com/zhentao/service/impl/CheckinServiceImpl.java

@@ -6,6 +6,8 @@ import com.zhentao.service.CheckinService;
 import com.zhentao.service.VipService;
 import com.zhentao.vo.CheckinInfoVO;
 import com.zhentao.vo.CheckinRewardVO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
@@ -20,11 +22,14 @@ import java.util.List;
 /**
  * 签到服务实现类
  * 支持普通用户和红娘分开签到统计
+ * 主要依赖数据库存储,Redis作为可选缓存(优雅降级)
  */
 @Service
 public class CheckinServiceImpl implements CheckinService {
     
-    @Autowired
+    private static final Logger logger = LoggerFactory.getLogger(CheckinServiceImpl.class);
+    
+    @Autowired(required = false)
     private RedisTemplate<String, Object> redisTemplate;
     
     @Autowired
@@ -43,6 +48,21 @@ public class CheckinServiceImpl implements CheckinService {
         return CHECKIN_KEY_PREFIX + userType + ":" + userId + ":" + yearMonth;
     }
     
+    /**
+     * 安全地设置Redis Bitmap(Redis不可用时跳过)
+     */
+    private void safeSetBit(String key, long offset, boolean value) {
+        if (redisTemplate == null) {
+            logger.debug("Redis未配置,跳过Bitmap设置");
+            return;
+        }
+        try {
+            redisTemplate.opsForValue().setBit(key, offset, value);
+        } catch (Exception e) {
+            logger.warn("Redis Bitmap设置失败,但不影响签到功能: {}", e.getMessage());
+        }
+    }
+    
     @Override
     public CheckinInfoVO checkin(Long userId) {
         // 默认普通用户签到
@@ -54,28 +74,24 @@ public class CheckinServiceImpl implements CheckinService {
     public CheckinInfoVO checkin(Long userId, Integer userType) {
         LocalDate today = LocalDate.now();
         
-        // 检查今天是否已签到(区分用户类型)
+        // 检查今天是否已签到(区分用户类型)- 从数据库检查
         CheckinRecord existRecord = checkinRecordMapper.selectByUserIdAndTypeAndDate(userId, userType, today);
         if (existRecord != null) {
             throw new RuntimeException("今日已签到");
         }
         
-        // 使用Redis Bitmap记录签到
-        String key = getCheckinKey(userId, userType, today);
-        int dayOfMonth = today.getDayOfMonth();
-        redisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
-        
-        // 计算连续签到天数(区分用户类型)
-        Integer continuousDays = calculateContinuousDays(userId, userType);
+        // 计算连续签到天数(区分用户类型)- 先计算,因为今天还没签到
+        // 需要检查昨天是否签到来判断是否连续
+        Integer continuousDays = calculateContinuousDaysBeforeToday(userId, userType) + 1;
         
         // 计算累计签到天数(区分用户类型)
-        Integer totalDays = calculateTotalDays(userId, userType);
+        Integer totalDays = calculateTotalDays(userId, userType) + 1;
         
         // 判断是否获得奖励
         Integer rewardType = getRewardType(continuousDays);
         Integer rewardReceived = rewardType > 0 ? 1 : 0;
         
-        // 保存签到记录到数据库
+        // 保存签到记录到数据库(主要存储)
         CheckinRecord record = new CheckinRecord();
         record.setUserId(userId);
         record.setUserType(userType);
@@ -87,11 +103,21 @@ public class CheckinServiceImpl implements CheckinService {
         record.setCreateTime(LocalDateTime.now());
         checkinRecordMapper.insert(record);
         
+        // 尝试使用Redis Bitmap记录签到(可选,失败不影响主流程)
+        String key = getCheckinKey(userId, userType, today);
+        int dayOfMonth = today.getDayOfMonth();
+        safeSetBit(key, dayOfMonth - 1, true);
+        
         // 如果获得VIP奖励,自动发放(仅普通用户)
         if (rewardType > 0 && userType == CheckinRecord.USER_TYPE_NORMAL) {
             Integer vipDays = getVipDays(rewardType);
             if (vipDays > 0) {
-                vipService.grantVip(userId, vipDays, "签到奖励-连续" + continuousDays + "天");
+                try {
+                    vipService.grantVip(userId, vipDays, "签到奖励-连续" + continuousDays + "天");
+                    logger.info("用户{}获得签到奖励:{}天VIP", userId, vipDays);
+                } catch (Exception e) {
+                    logger.error("发放VIP奖励失败: userId={}, days={}", userId, vipDays, e);
+                }
             }
         }
         
@@ -111,6 +137,10 @@ public class CheckinServiceImpl implements CheckinService {
         
         CheckinInfoVO vo = new CheckinInfoVO();
         
+        // 检查今天是否已签到(区分用户类型)
+        CheckinRecord todayRecord = checkinRecordMapper.selectByUserIdAndTypeAndDate(userId, userType, today);
+        vo.setTodayChecked(todayRecord != null);
+        
         // 计算连续签到天数(区分用户类型)
         Integer continuousDays = calculateContinuousDays(userId, userType);
         vo.setContinuousDays(continuousDays);
@@ -119,10 +149,6 @@ public class CheckinServiceImpl implements CheckinService {
         Integer totalDays = calculateTotalDays(userId, userType);
         vo.setTotalDays(totalDays);
         
-        // 检查今天是否已签到(区分用户类型)
-        CheckinRecord todayRecord = checkinRecordMapper.selectByUserIdAndTypeAndDate(userId, userType, today);
-        vo.setTodayChecked(todayRecord != null);
-        
         // 获取本月已签到日期(区分用户类型)
         List<String> checkedDates = getMonthCheckedDates(userId, userType, today);
         vo.setCheckedDates(checkedDates);
@@ -135,7 +161,7 @@ public class CheckinServiceImpl implements CheckinService {
     }
     
     /**
-     * 计算连续签到天数(区分用户类型
+     * 计算连续签到天数(包含今天,如果今天已签到
      */
     private Integer calculateContinuousDays(Long userId, Integer userType) {
         LocalDate today = LocalDate.now();
@@ -156,6 +182,28 @@ public class CheckinServiceImpl implements CheckinService {
         return continuousDays;
     }
     
+    /**
+     * 计算今天之前的连续签到天数(用于签到时计算)
+     */
+    private Integer calculateContinuousDaysBeforeToday(Long userId, Integer userType) {
+        LocalDate yesterday = LocalDate.now().minusDays(1);
+        int continuousDays = 0;
+        LocalDate checkDate = yesterday;
+        
+        // 从昨天往前推,检查连续签到
+        for (int i = 0; i < 365; i++) {
+            CheckinRecord record = checkinRecordMapper.selectByUserIdAndTypeAndDate(userId, userType, checkDate);
+            if (record != null) {
+                continuousDays++;
+                checkDate = checkDate.minusDays(1);
+            } else {
+                break;
+            }
+        }
+        
+        return continuousDays;
+    }
+    
     /**
      * 计算累计签到天数(区分用户类型)
      */

+ 81 - 0
service/Essential/src/main/java/com/zhentao/service/impl/TabbarConfigServiceImpl.java

@@ -0,0 +1,81 @@
+package com.zhentao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zhentao.entity.TabbarConfig;
+import com.zhentao.mapper.TabbarConfigMapper;
+import com.zhentao.service.TabbarConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 底部导航栏配置服务实现
+ */
+@Service
+public class TabbarConfigServiceImpl implements TabbarConfigService {
+    
+    @Autowired
+    private TabbarConfigMapper tabbarConfigMapper;
+    
+    private static final String CLIENT_TYPE_USER = "user";
+    private static final String CLIENT_TYPE_MATCHMAKER = "matchmaker";
+    
+    @Override
+    @Cacheable(value = "tabbarConfig", key = "'user'", unless = "#result == null || #result.isEmpty()")
+    public List<TabbarConfig> getUserTabbarConfig() {
+        return getTabbarConfig(CLIENT_TYPE_USER);
+    }
+    
+    @Override
+    @Cacheable(value = "tabbarConfig", key = "'matchmaker'", unless = "#result == null || #result.isEmpty()")
+    public List<TabbarConfig> getMatchmakerTabbarConfig() {
+        return getTabbarConfig(CLIENT_TYPE_MATCHMAKER);
+    }
+    
+    @Override
+    public List<TabbarConfig> getTabbarConfig(String clientType) {
+        return tabbarConfigMapper.selectEnabledByClientType(clientType);
+    }
+    
+    @Override
+    public List<TabbarConfig> getAllTabbarConfig(String clientType) {
+        return tabbarConfigMapper.selectAllByClientType(clientType);
+    }
+    
+    @Override
+    @CacheEvict(value = "tabbarConfig", allEntries = true)
+    public boolean updateTabbarConfig(TabbarConfig config) {
+        config.setUpdateTime(LocalDateTime.now());
+        return tabbarConfigMapper.updateById(config) > 0;
+    }
+    
+    @Override
+    @CacheEvict(value = "tabbarConfig", allEntries = true)
+    public boolean addTabbarConfig(TabbarConfig config) {
+        config.setCreateTime(LocalDateTime.now());
+        config.setUpdateTime(LocalDateTime.now());
+        return tabbarConfigMapper.insert(config) > 0;
+    }
+    
+    @Override
+    @CacheEvict(value = "tabbarConfig", allEntries = true)
+    public boolean deleteTabbarConfig(Long id) {
+        return tabbarConfigMapper.deleteById(id) > 0;
+    }
+    
+    @Override
+    @CacheEvict(value = "tabbarConfig", allEntries = true)
+    public boolean toggleTabbarItem(Long id, Boolean enabled) {
+        TabbarConfig config = tabbarConfigMapper.selectById(id);
+        if (config == null) {
+            return false;
+        }
+        config.setIsEnabled(enabled);
+        config.setUpdateTime(LocalDateTime.now());
+        return tabbarConfigMapper.updateById(config) > 0;
+    }
+}

+ 2 - 2
service/Essential/src/main/resources/application.yml

@@ -28,9 +28,9 @@ spring:
           enabled: false
   # Redis配置
   redis:
-    host: 115.190.125.125
+    host: 127.0.0.1
     port: 6377
-    password: # 如果有密码请填写
+    password: Futu@2028@
     database: 0  # 使用数据库0
     timeout: 3000ms
     lettuce:

+ 33 - 0
service/Essential/src/main/resources/sql/tabbar_config.sql

@@ -0,0 +1,33 @@
+-- 底部导航栏配置表
+CREATE TABLE IF NOT EXISTS `tabbar_config` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `name` varchar(50) NOT NULL COMMENT '导航项名称',
+  `icon` varchar(100) NOT NULL COMMENT '图标(emoji或图片URL)',
+  `icon_selected` varchar(100) DEFAULT NULL COMMENT '选中状态图标',
+  `path` varchar(200) NOT NULL COMMENT '跳转路径',
+  `tab_key` varchar(50) NOT NULL COMMENT '导航项标识(如index, plaza, recommend, mine)',
+  `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序顺序(越小越靠前)',
+  `is_enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用:0-禁用,1-启用',
+  `badge_type` varchar(20) DEFAULT NULL COMMENT '角标类型:none-无,dot-红点,number-数字',
+  `badge_key` varchar(50) DEFAULT NULL COMMENT '角标数据key(用于获取动态数据,如unreadCount)',
+  `client_type` varchar(20) NOT NULL DEFAULT 'user' COMMENT '客户端类型:user-用户端,matchmaker-红娘端',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_client_type` (`client_type`),
+  KEY `idx_sort_order` (`sort_order`),
+  KEY `idx_is_enabled` (`is_enabled`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='底部导航栏配置表';
+
+-- 插入用户端默认导航配置
+INSERT INTO `tabbar_config` (`name`, `icon`, `icon_selected`, `path`, `tab_key`, `sort_order`, `is_enabled`, `badge_type`, `badge_key`, `client_type`) VALUES
+('首页', '🏠', '🏠', '/pages/index/index', 'index', 1, 1, 'none', NULL, 'user'),
+('动态', '💕', '💕', '/pages/plaza/index', 'plaza', 2, 1, 'none', NULL, 'user'),
+('推荐', '👍', '👍', '/pages/recommend/index', 'recommend', 3, 1, 'none', NULL, 'user'),
+('我的', '👤', '👤', '/pages/mine/index', 'mine', 4, 1, 'none', NULL, 'user');
+
+-- 插入红娘端默认导航配置(可选)
+INSERT INTO `tabbar_config` (`name`, `icon`, `icon_selected`, `path`, `tab_key`, `sort_order`, `is_enabled`, `badge_type`, `badge_key`, `client_type`) VALUES
+('工作台', '🏠', '🏠', '/pages/matchmaker-workbench/index', 'workbench', 1, 1, 'none', NULL, 'matchmaker'),
+('消息', '💬', '💬', '/pages/matchmaker-workbench/message', 'message', 2, 1, 'number', 'unreadCount', 'matchmaker'),
+('我的', '👤', '👤', '/pages/matchmaker-workbench/mine', 'mine', 3, 1, 'none', NULL, 'matchmaker');

+ 439 - 242
service/dynamic/src/main/java/com/zhentao/controller/ContentSecurityController.java

@@ -1,408 +1,605 @@
 package com.zhentao.controller;
 
-import com.zhentao.common.Result;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.core.io.ByteArrayResource;
-import org.springframework.http.*;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.util.MultiValueMap;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.web.client.RestTemplate;
-
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.*;
 import java.net.HttpURLConnection;
 import java.net.URL;
-import java.util.*;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
  * 内容安全检测控制器
  * 用于检测用户发布的文字、图片是否包含违规内容
- * 集成微信小程序内容安全API
+ * 使用微信小程序内容安全API
  */
 @RestController
-@RequestMapping("/content-security")
+@RequestMapping("/api/content-security")
 public class ContentSecurityController {
 
     private static final Logger logger = LoggerFactory.getLogger(ContentSecurityController.class);
 
+    // ====================== 常量配置 (统一维护,无硬编码) ======================
+    private static final int MAX_IMAGE_SIZE = 2 * 1024 * 1024; // 图片最大下载大小 2MB,防止OOM
+    private static final int WX_IMAGE_LIMIT_SIZE = 1 * 1024 * 1024; // 微信同步检测图片限制1MB
+    private static final int TOKEN_EXPIRE_ADVANCE = 300; // token提前300秒刷新
+    private static final int WX_ERRCODE_ILLEGAL_CONTENT = 87014; // 内容违规错误码
+    private static final int WX_ERRCODE_TOKEN_EXPIRE_1 = 40001; // token过期错误码1
+    private static final int WX_ERRCODE_TOKEN_EXPIRE_2 = 42001; // token过期错误码2
+
     @Value("${wechat.miniapp.appid:}")
     private String appId;
 
     @Value("${wechat.miniapp.secret:}")
     private String appSecret;
 
-    private final RestTemplate restTemplate = new RestTemplate();
-    private final ObjectMapper objectMapper = new ObjectMapper();
-
-    private String cachedAccessToken;
-    private long tokenExpireTime = 0;
+    // AccessToken缓存,volatile保证多线程可见性
+    private volatile String cachedAccessToken;
+    private volatile long tokenExpireTime = 0;
 
     private static final List<String> SENSITIVE_WORDS = Arrays.asList(
-        "色情", "裸体", "性爱", "约炮", "一夜情", "援交", "卖淫", "嫖娼", "小姐服务",
-        "做爱", "性交", "口交", "肛交", "自慰", "手淫", "阴茎", "阴道", "乳房",
-        "傻逼", "操你", "草泥马", "妈的", "他妈的", "狗日的", "王八蛋", "贱人", "婊子",
-        "滚蛋", "去死", "废物", "垃圾", "白痴", "智障",
-        "赌博", "毒品", "枪支", "炸弹", "恐怖", "暴力", "杀人", "自杀",
-        "刷单", "兼职日结", "高额返利", "免费领取", "中奖", "彩票内幕", "稳赚不赔",
-        "法轮功", "邪教"
+            "色情", "裸体", "性爱", "约炮", "一夜情", "援交", "卖淫", "嫖娼", "小姐服务",
+            "做爱", "性交", "口交", "肛交", "自慰", "手淫",
+            "傻逼", "操你", "草泥马", "妈的", "他妈的", "狗日的", "王八蛋", "贱人", "婊子",
+            "滚蛋", "去死", "废物", "白痴", "智障",
+            "赌博", "毒品", "枪支", "炸弹", "恐怖", "暴力", "杀人", "自杀",
+            "刷单", "兼职日结", "高额返利", "免费领取", "中奖", "彩票内幕", "稳赚不赔",
+            "法轮功", "邪教"
     );
 
-    private static final List<Pattern> SENSITIVE_PATTERNS = new ArrayList<>();
-
-    static {
-        for (String word : SENSITIVE_WORDS) {
-            StringBuilder patternStr = new StringBuilder();
-            for (int i = 0; i < word.length(); i++) {
-                if (i > 0) {
-                    patternStr.append("[\\s\\*\\-\\_\\.]*");
-                }
-                patternStr.append(Pattern.quote(String.valueOf(word.charAt(i))));
-            }
-            SENSITIVE_PATTERNS.add(Pattern.compile(patternStr.toString(), Pattern.CASE_INSENSITIVE));
-        }
-    }
-
     /**
      * 检测文字内容是否安全
      */
-    @PostMapping("/check-text")
-    public Result<Map<String, Object>> checkText(@RequestBody Map<String, String> request) {
-        String content = request.get("content");
-        
+    @PostMapping(value = "/check-text", produces = MediaType.APPLICATION_JSON_VALUE)
+    @ResponseBody
+    public String checkText(HttpServletRequest request) {
+        String content = null;
+        try {
+            String body = readRequestBody(request);
+            content = getJsonString(body, "content");
+        } catch (Exception e) {
+            logger.error("读取文字检测请求体失败", e);
+            return buildResponse(false, "请求参数解析失败");
+        }
+
         if (content == null || content.trim().isEmpty()) {
-            return Result.success(createSafeResult());
+            return buildResponse(true, "");
         }
 
+        logger.info("开始检测文字内容,长度: {}", content.length());
+
+        // 本地敏感词检测
         String localCheckResult = localTextCheck(content);
         if (localCheckResult != null) {
             logger.info("本地敏感词检测不通过: {}", localCheckResult);
-            return Result.success(createUnsafeResult("内容包含敏感词,请修改后重试"));
+            return buildResponse(false, "内容包含敏感词,请修改后重试");
         }
 
+        // 微信内容安全检测
         try {
-            Map<String, Object> wxResult = wxMsgSecCheck(content);
-            if (wxResult != null && Boolean.FALSE.equals(wxResult.get("safe"))) {
+            String wxResult = wxMsgSecCheck(content);
+            if (wxResult != null) {
                 logger.info("微信内容安全检测不通过");
-                return Result.success(createUnsafeResult((String) wxResult.get("message")));
+                return buildResponse(false, wxResult);
             }
         } catch (Exception e) {
-            logger.error("微信内容安全检测异常", e);
+            logger.error("微信文字内容安全检测异常", e);
         }
 
-        return Result.success(createSafeResult());
+        logger.info("文字内容检测通过");
+        return buildResponse(true, "");
     }
 
     /**
-     * 检测图片是否安全(同步检测)
+     * 检测图片是否安全
+     * 检测链路:同步接口(img_sec_check) → 异步接口(media_check_async)
      */
-    @PostMapping("/check-image")
-    public Result<Map<String, Object>> checkImage(@RequestBody Map<String, String> request) {
-        String imageUrl = request.get("imageUrl");
-        
+    @PostMapping(value = "/check-image", produces = MediaType.APPLICATION_JSON_VALUE)
+    @ResponseBody
+    public String checkImage(HttpServletRequest request) {
+        String imageUrl = null;
+        try {
+            String body = readRequestBody(request);
+            logger.info("收到图片检测请求: {}", body);
+            imageUrl = getJsonString(body, "imageUrl");
+        } catch (Exception e) {
+            logger.error("读取图片检测请求体失败", e);
+            return buildResponse(false, "请求参数错误");
+        }
+
         if (imageUrl == null || imageUrl.trim().isEmpty()) {
-            return Result.success(createSafeResult());
+            logger.warn("图片URL为空,跳过检测");
+            return buildResponse(true, "");
         }
 
         logger.info("开始检测图片: {}", imageUrl);
 
+        // 1. 先尝试使用微信同步接口 img_sec_check (精度高,实时返回结果)
+        try {
+            String wxResult = wxImgSecCheck(imageUrl);
+            if (wxResult != null) {
+                logger.info("微信图片同步检测不通过: {}", imageUrl);
+                return buildResponse(false, wxResult);
+            }
+        } catch (Exception e) {
+            logger.error("微信图片同步检测异常: {}", imageUrl, e);
+        }
+
+        // 2. 如果同步接口无结果/图片过大,使用异步接口补充检测
         try {
-            // 下载图片并调用微信图片安全检测API
-            Map<String, Object> wxResult = wxImgSecCheck(imageUrl);
-            if (wxResult != null && Boolean.FALSE.equals(wxResult.get("safe"))) {
-                logger.info("微信图片安全检测不通过: {}", imageUrl);
-                return Result.success(createUnsafeResult((String) wxResult.get("message")));
+            String asyncResult = wxMediaCheckAsync(imageUrl);
+            if (asyncResult != null) {
+                logger.info("微信图片异步检测不通过: {}", imageUrl);
+                return buildResponse(false, asyncResult);
             }
-            logger.info("图片检测通过: {}", imageUrl);
         } catch (Exception e) {
-            logger.error("微信图片安全检测异常: {}", imageUrl, e);
-            // 异常时默认通过,由人工审核
+            logger.error("微信图片异步检测异常: {}", imageUrl, e);
         }
 
-        return Result.success(createSafeResult());
+        logger.info("图片检测通过: {}", imageUrl);
+        return buildResponse(true, "");
     }
 
     /**
-     * 批量检测图片
+     * 构建统一JSON响应体
      */
-    @PostMapping("/check-images")
-    public Result<Map<String, Object>> checkImages(@RequestBody Map<String, Object> request) {
-        @SuppressWarnings("unchecked")
-        List<String> imageUrls = (List<String>) request.get("imageUrls");
-        
-        if (imageUrls == null || imageUrls.isEmpty()) {
-            return Result.success(createSafeResult());
-        }
-
-        for (int i = 0; i < imageUrls.size(); i++) {
-            String imageUrl = imageUrls.get(i);
-            if (imageUrl == null || imageUrl.trim().isEmpty()) {
-                continue;
-            }
+    private String buildResponse(boolean safe, String message) {
+        return "{\"code\":200,\"message\":\"成功\",\"data\":{\"safe\":" + safe +
+                ",\"message\":\"" + escapeJson(message) + "\"},\"timestamp\":" +
+                System.currentTimeMillis() + "}";
+    }
 
-            try {
-                Map<String, Object> wxResult = wxImgSecCheck(imageUrl);
-                if (wxResult != null && Boolean.FALSE.equals(wxResult.get("safe"))) {
-                    Map<String, Object> result = createUnsafeResult("第" + (i + 1) + "张图片包含违规内容,请更换后重试");
-                    result.put("failedIndex", i);
-                    return Result.success(result);
-                }
-            } catch (Exception e) {
-                logger.error("检测第{}张图片异常: {}", i + 1, imageUrl, e);
+    /**
+     * 读取request请求体
+     */
+    private String readRequestBody(HttpServletRequest request) throws IOException {
+        StringBuilder sb = new StringBuilder();
+        try (BufferedReader reader = request.getReader()) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
             }
         }
-
-        return Result.success(createSafeResult());
+        return sb.toString();
     }
 
+    /**
+     * 本地敏感词检测-忽略大小写
+     */
     private String localTextCheck(String content) {
         String lowerContent = content.toLowerCase();
-        
         for (String word : SENSITIVE_WORDS) {
             if (lowerContent.contains(word.toLowerCase())) {
                 return word;
             }
         }
-        
-        for (int i = 0; i < SENSITIVE_PATTERNS.size(); i++) {
-            if (SENSITIVE_PATTERNS.get(i).matcher(content).find()) {
-                return SENSITIVE_WORDS.get(i);
-            }
-        }
-
         return null;
     }
 
     /**
-     * 调用微信文字内容安全检测API
+     * 调用微信文字内容安全检测API (msg_sec_check) - 同步接口
      */
-    private Map<String, Object> wxMsgSecCheck(String content) {
+    private String wxMsgSecCheck(String content) {
+        HttpURLConnection conn = null;
         try {
             String accessToken = getAccessToken();
             if (accessToken == null || accessToken.isEmpty()) {
-                logger.warn("获取access_token失败,跳过微信内容检测");
+                logger.warn("获取access_token失败,跳过微信文字检测");
                 return null;
             }
 
-            String url = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + accessToken;
-
-            HttpHeaders headers = new HttpHeaders();
-            headers.setContentType(MediaType.APPLICATION_JSON);
-
-            Map<String, Object> body = new HashMap<>();
-            body.put("content", content);
-            body.put("version", 2);
-            body.put("scene", 2);
+            String urlStr = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + accessToken;
+            URL url = new URL(urlStr);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
+            conn.setDoOutput(true);
+            conn.setConnectTimeout(10000);
+            conn.setReadTimeout(10000);
+
+            // 修复:openid传空字符串(无用户openid时的兼容写法,微信允许),不再传appId
+            String jsonBody = "{\"content\":\"" + escapeJson(content) + "\",\"version\":2,\"scene\":2,\"openid\":\"\"}";
+            logger.debug("微信文字检测请求体: {}", jsonBody);
+
+            try (OutputStream os = conn.getOutputStream()) {
+                os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
+            }
 
-            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
-            ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
+            int responseCode = conn.getResponseCode();
+            String responseBody = readHttpResponse(conn);
+            logger.info("微信文字检测响应: code={}, body={}", responseCode, responseBody);
 
-            if (response.getStatusCode() == HttpStatus.OK) {
-                JsonNode jsonNode = objectMapper.readTree(response.getBody());
-                int errcode = jsonNode.has("errcode") ? jsonNode.get("errcode").asInt() : 0;
-                
+            if (responseCode == 200 && responseBody != null) {
+                int errcode = getJsonInt(responseBody, "errcode");
+                // 全局处理token过期
+                if (isTokenExpireCode(errcode)) {
+                    clearTokenCache();
+                    return null;
+                }
                 if (errcode == 0) {
-                    JsonNode result = jsonNode.get("result");
-                    if (result != null) {
-                        String suggest = result.has("suggest") ? result.get("suggest").asText() : "pass";
-                        if ("risky".equals(suggest)) {
-                            String label = result.has("label") ? result.get("label").asText() : "违规内容";
-                            return createUnsafeResult("内容包含" + getLabelText(label) + ",请修改后重试");
-                        }
+                    String suggest = getNestedJsonString(responseBody, "result", "suggest");
+                    if ("risky".equals(suggest)) {
+                        String label = getNestedJsonString(responseBody, "result", "label");
+                        return "内容包含" + getLabelText(label) + ",请修改后重试";
                     }
-                    return createSafeResult();
-                } else if (errcode == 87014) {
-                    return createUnsafeResult("内容包含违规信息,请修改后重试");
+                    return null;
+                } else if (errcode == WX_ERRCODE_ILLEGAL_CONTENT) {
+                    return "内容包含违规信息,请修改后重试";
                 } else {
-                    logger.warn("微信内容检测返回错误码: {}", errcode);
+                    logger.warn("微信文字检测返回错误码: {}, 描述: {}", errcode, getJsonString(responseBody, "errmsg"));
                 }
             }
         } catch (Exception e) {
-            logger.error("调用微信文字内容安全API异常", e);
+            logger.error("调用微信文字安全检测API异常", e);
+        } finally {
+            if (conn != null) conn.disconnect();
         }
         return null;
     }
 
     /**
-     * 调用微信图片内容安全检测API(同步,通过上传图片文件)
-     * https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/sec-center/sec-check/imgSecCheck.html
+     * 调用微信图片内容安全检测API (img_sec_check) - 同步接口
+     * 限制:图片大小 <= 1MB,实时返回检测结果
      */
-    private Map<String, Object> wxImgSecCheck(String imageUrl) {
+    private String wxImgSecCheck(String imageUrl) {
+        HttpURLConnection conn = null;
         try {
             String accessToken = getAccessToken();
             if (accessToken == null || accessToken.isEmpty()) {
-                logger.warn("获取access_token失败,跳过微信图片检测");
+                logger.warn("获取access_token失败,跳过微信图片同步检测");
                 return null;
             }
 
-            // 1. 下载图片
+            logger.info("开始下载图片: {}", imageUrl);
             byte[] imageBytes = downloadImage(imageUrl);
             if (imageBytes == null || imageBytes.length == 0) {
                 logger.warn("下载图片失败: {}", imageUrl);
                 return null;
             }
+            logger.info("图片下载成功,大小: {} bytes", imageBytes.length);
 
-            // 检查图片大小(微信限制1MB)
-            if (imageBytes.length > 1024 * 1024) {
-                logger.warn("图片大小超过1MB,跳过微信检测: {} bytes", imageBytes.length);
-                // 大图片跳过微信检测,由人工审核
-                return createSafeResult();
+            // 微信同步检测限制图片1MB,超过则跳过走异步
+            if (imageBytes.length > WX_IMAGE_LIMIT_SIZE) {
+                logger.warn("图片大小超过1MB({} bytes),跳过同步检测,走异步检测", imageBytes.length);
+                return null;
             }
 
-            // 2. 上传到微信进行检测
-            String url = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=" + accessToken;
+            String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();
+            String urlStr = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=" + accessToken;
+
+            URL url = new URL(urlStr);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
+            conn.setDoOutput(true);
+            conn.setConnectTimeout(15000);
+            conn.setReadTimeout(30000);
+
+            try (OutputStream os = conn.getOutputStream()) {
+                String header = "--" + boundary + "\r\n" +
+                        "Content-Disposition: form-data; name=\"media\"; filename=\"image.jpg\"\r\n" +
+                        "Content-Type: application/octet-stream\r\n\r\n";
+                os.write(header.getBytes(StandardCharsets.UTF_8));
+                os.write(imageBytes);
+                os.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
+                os.flush();
+            }
 
-            HttpHeaders headers = new HttpHeaders();
-            headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+            int responseCode = conn.getResponseCode();
+            String responseBody = readHttpResponse(conn);
+            logger.info("微信图片同步检测响应: code={}, body={}", responseCode, responseBody);
 
-            // 创建文件资源
-            ByteArrayResource fileResource = new ByteArrayResource(imageBytes) {
-                @Override
-                public String getFilename() {
-                    return "image.jpg";
+            if (responseCode == 200 && responseBody != null) {
+                int errcode = getJsonInt(responseBody, "errcode");
+                if (isTokenExpireCode(errcode)) {
+                    clearTokenCache();
+                    return null;
                 }
-            };
+                if (errcode == 0) {
+                    return null;
+                } else if (errcode == WX_ERRCODE_ILLEGAL_CONTENT) {
+                    return "图片包含违规内容,请更换后重试";
+                } else {
+                    logger.warn("微信图片同步检测错误码: {}, 描述: {}", errcode, getJsonString(responseBody, "errmsg"));
+                }
+            }
+        } catch (Exception e) {
+            logger.error("调用微信图片同步检测API异常", e);
+        } finally {
+            if (conn != null) conn.disconnect();
+        }
+        return null;
+    }
 
-            MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
-            body.add("media", fileResource);
+    /**
+     * 调用微信媒体内容安全检测API (media_check_async) - 异步接口
+     * 适用:图片>1MB,提交检测后返回trace_id,结果需回调获取
+     */
+    private String wxMediaCheckAsync(String imageUrl) {
+        HttpURLConnection conn = null;
+        try {
+            String accessToken = getAccessToken();
+            if (accessToken == null || accessToken.isEmpty()) {
+                logger.warn("获取access_token失败,跳过微信图片异步检测");
+                return null;
+            }
 
-            HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
-            ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
+            String urlStr = "https://api.weixin.qq.com/wxa/media_check_async?access_token=" + accessToken;
+            URL url = new URL(urlStr);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
+            conn.setDoOutput(true);
+            conn.setConnectTimeout(10000);
+            conn.setReadTimeout(10000);
+
+            // 修复:openid传空字符串,不再传appId
+            String jsonBody = "{\"media_url\":\"" + escapeJson(imageUrl) + "\",\"media_type\":2,\"version\":2,\"scene\":2,\"openid\":\"\"}";
+            logger.info("微信图片异步检测请求体: {}", jsonBody);
+
+            try (OutputStream os = conn.getOutputStream()) {
+                os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
+            }
 
-            logger.info("微信图片检测响应: {}", response.getBody());
+            int responseCode = conn.getResponseCode();
+            String responseBody = readHttpResponse(conn);
+            logger.info("微信图片异步检测响应: code={}, body={}", responseCode, responseBody);
 
-            if (response.getStatusCode() == HttpStatus.OK) {
-                JsonNode jsonNode = objectMapper.readTree(response.getBody());
-                int errcode = jsonNode.has("errcode") ? jsonNode.get("errcode").asInt() : 0;
-                
+            if (responseCode == 200 && responseBody != null) {
+                int errcode = getJsonInt(responseBody, "errcode");
+                if (isTokenExpireCode(errcode)) {
+                    clearTokenCache();
+                    return null;
+                }
                 if (errcode == 0) {
-                    // 检测通过
-                    return createSafeResult();
-                } else if (errcode == 87014) {
-                    // 图片含有违法违规内容
-                    return createUnsafeResult("图片包含违规内容,请更换后重试");
+                    String traceId = getJsonString(responseBody, "trace_id");
+                    logger.info("微信异步检测已提交,trace_id: {}", traceId);
+                    return null;
+                } else if (errcode == WX_ERRCODE_ILLEGAL_CONTENT) {
+                    return "图片包含违规内容,请更换后重试";
                 } else {
-                    logger.warn("微信图片检测返回错误码: {}, errmsg: {}", 
-                        errcode, jsonNode.has("errmsg") ? jsonNode.get("errmsg").asText() : "");
+                    logger.warn("微信异步检测错误码: {}, 描述: {}", errcode, getJsonString(responseBody, "errmsg"));
                 }
             }
         } catch (Exception e) {
-            logger.error("调用微信图片内容安全API异常", e);
+            logger.error("调用微信图片异步检测API异常", e);
+        } finally {
+            if (conn != null) conn.disconnect();
         }
         return null;
     }
 
     /**
-     * 下载图片
+     * 下载图片字节数组,增加大小限制防止OOM
      */
     private byte[] downloadImage(String imageUrl) {
-        HttpURLConnection connection = null;
-        InputStream inputStream = null;
-        ByteArrayOutputStream outputStream = null;
-        
+        HttpURLConnection conn = null;
         try {
             URL url = new URL(imageUrl);
-            connection = (HttpURLConnection) url.openConnection();
-            connection.setRequestMethod("GET");
-            connection.setConnectTimeout(10000);
-            connection.setReadTimeout(30000);
-            connection.setRequestProperty("User-Agent", "Mozilla/5.0");
-            
-            int responseCode = connection.getResponseCode();
-            if (responseCode != HttpURLConnection.HTTP_OK) {
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("GET");
+            conn.setConnectTimeout(10000);
+            conn.setReadTimeout(30000);
+            conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
+            conn.setInstanceFollowRedirects(true);
+
+            int responseCode = conn.getResponseCode();
+            if (responseCode != 200) {
                 logger.warn("下载图片失败,HTTP状态码: {}", responseCode);
                 return null;
             }
 
-            inputStream = connection.getInputStream();
-            outputStream = new ByteArrayOutputStream();
-            
-            byte[] buffer = new byte[4096];
-            int bytesRead;
-            while ((bytesRead = inputStream.read(buffer)) != -1) {
-                outputStream.write(buffer, 0, bytesRead);
+            try (InputStream is = conn.getInputStream();
+                 ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+                byte[] buffer = new byte[8192];
+                int bytesRead;
+                while ((bytesRead = is.read(buffer)) != -1) {
+                    // 超过最大限制,直接返回null,防止OOM
+                    if (baos.size() + bytesRead > MAX_IMAGE_SIZE) {
+                        logger.warn("图片大小超过{}MB,终止下载", MAX_IMAGE_SIZE / 1024 / 1024);
+                        return null;
+                    }
+                    baos.write(buffer, 0, bytesRead);
+                }
+                return baos.toByteArray();
             }
-            
-            return outputStream.toByteArray();
         } catch (Exception e) {
             logger.error("下载图片异常: {}", imageUrl, e);
             return null;
         } finally {
-            try {
-                if (inputStream != null) inputStream.close();
-                if (outputStream != null) outputStream.close();
-                if (connection != null) connection.disconnect();
-            } catch (Exception e) {
-                // ignore
-            }
+            if (conn != null) conn.disconnect();
         }
     }
 
-    private synchronized String getAccessToken() {
+    /**
+     * 获取微信AccessToken,双重检查锁保证并发安全+性能,缓存过期自动刷新
+     */
+    private String getAccessToken() {
+        // 第一次无锁检查,提高并发性能
         if (cachedAccessToken != null && System.currentTimeMillis() < tokenExpireTime) {
+            logger.debug("使用缓存的access_token,剩余有效期:{}ms", tokenExpireTime - System.currentTimeMillis());
             return cachedAccessToken;
         }
 
-        if (appId == null || appId.isEmpty() || appSecret == null || appSecret.isEmpty()) {
-            logger.warn("微信小程序appId或appSecret未配置");
+        synchronized (this) {
+            // 第二次加锁检查,防止多线程重复请求
+            if (cachedAccessToken != null && System.currentTimeMillis() < tokenExpireTime) {
+                return cachedAccessToken;
+            }
+
+            if (appId == null || appId.isEmpty() || appSecret == null || appSecret.isEmpty()) {
+                logger.error("微信小程序appId或appSecret未配置! appId={}", appId != null ? appId.substring(0, 4) + "***" : "null");
+                return null;
+            }
+
+            HttpURLConnection conn = null;
+            try {
+                String urlStr = String.format(
+                        "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
+                        appId, appSecret
+                );
+                logger.info("开始请求微信access_token,appId:{}***", appId.substring(0, 4));
+
+                URL url = new URL(urlStr);
+                conn = (HttpURLConnection) url.openConnection();
+                conn.setRequestMethod("GET");
+                conn.setConnectTimeout(10000);
+                conn.setReadTimeout(10000);
+
+                int responseCode = conn.getResponseCode();
+                String responseBody = readHttpResponse(conn);
+                logger.info("获取token响应: code={}, body={}", responseCode, responseBody);
+
+                if (responseCode == 200 && responseBody != null) {
+                    String token = getJsonString(responseBody, "access_token");
+                    if (token != null && !token.isEmpty()) {
+                        cachedAccessToken = token;
+                        int expiresIn = getJsonInt(responseBody, "expires_in");
+                        if (expiresIn == 0) expiresIn = 7200;
+                        tokenExpireTime = System.currentTimeMillis() + (expiresIn - TOKEN_EXPIRE_ADVANCE) * 1000L;
+                        logger.info("获取access_token成功,有效期:{}秒", expiresIn);
+                        return cachedAccessToken;
+                    } else {
+                        int errcode = getJsonInt(responseBody, "errcode");
+                        String errmsg = getJsonString(responseBody, "errmsg");
+                        logger.error("获取token失败: errcode={}, errmsg={}", errcode, errmsg);
+                    }
+                }
+            } catch (Exception e) {
+                logger.error("获取微信access_token异常", e);
+            } finally {
+                if (conn != null) conn.disconnect();
+            }
             return null;
         }
+    }
 
-        try {
-            String url = String.format(
-                "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
-                appId, appSecret
-            );
-
-            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
-            if (response.getStatusCode() == HttpStatus.OK) {
-                JsonNode jsonNode = objectMapper.readTree(response.getBody());
-                if (jsonNode.has("access_token")) {
-                    cachedAccessToken = jsonNode.get("access_token").asText();
-                    int expiresIn = jsonNode.has("expires_in") ? jsonNode.get("expires_in").asInt() : 7200;
-                    tokenExpireTime = System.currentTimeMillis() + (expiresIn - 300) * 1000L;
-                    return cachedAccessToken;
-                } else {
-                    logger.error("获取access_token失败: {}", response.getBody());
-                }
+    // ====================== 通用工具方法 ======================
+    /**
+     * 读取Http响应体,兼容成功流和错误流
+     */
+    private String readHttpResponse(HttpURLConnection conn) throws IOException {
+        InputStream is = conn.getResponseCode() >= 400 ? conn.getErrorStream() : conn.getInputStream();
+        if (is == null) return "";
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
             }
-        } catch (Exception e) {
-            logger.error("获取access_token异常", e);
+            return sb.toString();
+        } finally {
+            if (is != null) is.close();
+        }
+    }
+
+    /**
+     * 标准JSON字符串转义,补全所有缺失的转义字符
+     */
+    private String escapeJson(String str) {
+        if (str == null) return "";
+        StringBuilder sb = new StringBuilder();
+        for (char c : str.toCharArray()) {
+            switch (c) {
+                case '\"': sb.append("\\\""); break;
+                case '\\': sb.append("\\\\"); break;
+                case '/': sb.append("\\/"); break;
+                case '\b': sb.append("\\b"); break;
+                case '\f': sb.append("\\f"); break;
+                case '\n': sb.append("\\n"); break;
+                case '\r': sb.append("\\r"); break;
+                case '\t': sb.append("\\t"); break;
+                default: sb.append(c); break;
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 正则解析JSON字符串中的字符串值,兼容空格/换行
+     */
+    private String getJsonString(String json, String key) {
+        if (json == null || key == null) return null;
+        Pattern pattern = Pattern.compile("\"" + key + "\"\\s*:\\s*\"([^\"]*)\"");
+        Matcher matcher = pattern.matcher(json);
+        return matcher.find() ? matcher.group(1) : null;
+    }
+
+    /**
+     * 正则解析JSON字符串中的数字值
+     */
+    private int getJsonInt(String json, String key) {
+        if (json == null || key == null) return 0;
+        Pattern pattern = Pattern.compile("\"" + key + "\"\\s*:\\s*(-?\\d+)");
+        Matcher matcher = pattern.matcher(json);
+        if (matcher.find()) {
+            try {
+                return Integer.parseInt(matcher.group(1));
+            } catch (NumberFormatException e) {
+                return 0;
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * 解析嵌套JSON中的字符串值
+     */
+    private String getNestedJsonString(String json, String parentKey, String childKey) {
+        if (json == null || parentKey == null || childKey == null) return null;
+        Pattern parentPattern = Pattern.compile("\"" + parentKey + "\"\\s*:\\s*\\{([^}]*)\\}");
+        Matcher parentMatcher = parentPattern.matcher(json);
+        if (parentMatcher.find()) {
+            String parentContent = parentMatcher.group(1);
+            return getJsonString("{" + parentContent + "}", childKey);
         }
         return null;
     }
 
+    /**
+     * 新增:缺失的标签转换方法,解决运行报错
+     */
     private String getLabelText(String label) {
-        Map<String, String> labelMap = new HashMap<>();
-        labelMap.put("100", "违规内容");
-        labelMap.put("10001", "广告内容");
-        labelMap.put("20001", "时政内容");
-        labelMap.put("20002", "色情内容");
-        labelMap.put("20003", "辱骂内容");
-        labelMap.put("20006", "违法犯罪内容");
-        labelMap.put("20008", "欺诈内容");
-        labelMap.put("20012", "低俗内容");
-        labelMap.put("20013", "版权内容");
-        labelMap.put("21000", "其他违规内容");
-        return labelMap.getOrDefault(label, "违规内容");
+        if (label == null) return "违规内容";
+        switch (label) {
+            case "100": return "违规内容";
+            case "10001": return "广告推广内容";
+            case "20001": return "时政敏感内容";
+            case "20002": return "色情低俗内容";
+            case "20003": return "辱骂攻击内容";
+            case "20006": return "违法犯罪内容";
+            case "20008": return "欺诈诱导内容";
+            case "20012": return "低俗恶心内容";
+            case "20013": return "侵权版权内容";
+            case "21000": return "其他违规内容";
+            default: return "违规内容";
+        }
     }
 
-    private Map<String, Object> createSafeResult() {
-        Map<String, Object> result = new HashMap<>();
-        result.put("safe", true);
-        result.put("message", "");
-        return result;
+    /**
+     * 判断是否为token过期错误码
+     */
+    private boolean isTokenExpireCode(int errcode) {
+        return errcode == WX_ERRCODE_TOKEN_EXPIRE_1 || errcode == WX_ERRCODE_TOKEN_EXPIRE_2;
     }
 
-    private Map<String, Object> createUnsafeResult(String message) {
-        Map<String, Object> result = new HashMap<>();
-        result.put("safe", false);
-        result.put("message", message);
-        return result;
+    /**
+     * 清空token缓存
+     */
+    private void clearTokenCache() {
+        cachedAccessToken = null;
+        tokenExpireTime = 0;
+        logger.info("检测到access_token过期,已清空缓存");
     }
 }