From 62f8d49c054823813dd2f5fa7dd1aee12b7cd84c Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期三, 08 四月 2026 18:27:34 +0800
Subject: [PATCH] feat:App首页-Banner

---
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/statistics/controller/AppUserEntryLogController.java |   14 +
 traditionalApp/pages/home/home.vue                                                                                       |  228 ++++++++++++++++++------
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppHomeBannerController.java         |   37 ++++
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppHomeBannerVO.java                         |   24 ++
 traditionalApp/utils/request.js                                                                                          |   71 +++++--
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java                |    3 
 traditionalApp/pages/home/service.js                                                                                     |   19 +
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java              |   22 ++
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserFeedbackController.java       |   11 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java                             |   17 -
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppHomeBannerService.java               |   59 ++++++
 11 files changed, 386 insertions(+), 119 deletions(-)

diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java
index 14d59f6..b2d84c6 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java
@@ -18,6 +18,28 @@
     }
 
     /**
+     * 判断当前请求是否存在 APP 登录态。
+     */
+    public boolean isLogin() {
+        return StpAppUserUtil.isLogin();
+    }
+
+    /**
+     * 获取当前 APP 登录用户 ID。
+     */
+    public Long getLoginUserId() {
+        return StpAppUserUtil.getLoginIdAsLong();
+    }
+
+    /**
+     * 获取当前 APP 登录用户 ID,不存在时抛出未登录异常。
+     */
+    public Long requireLoginUserId() {
+        StpAppUserUtil.checkLogin();
+        return StpAppUserUtil.getLoginIdAsLong();
+    }
+
+    /**
      * 获取当前 APP 登录态的 token 信息。
      */
     public SaTokenInfo getTokenInfo() {
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java
index 84e50e3..c8de56a 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java
@@ -6,6 +6,7 @@
 import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestOperations;
 import org.springframework.web.client.RestTemplate;
@@ -26,12 +27,14 @@
     /**
      * 使用默认 HTTP 客户端构造微信认证客户端。
      */
+    @Autowired
     public AppWechatAuthClient(AppWechatAuthProperties properties, ObjectMapper objectMapper) {
         this(properties, objectMapper, new RestTemplate());
     }
 
     /**
      * 注入微信配置、JSON 解析器与 HTTP 客户端。
+     * 存在多个构造器时,这个扩展构造器仅供测试或内部定制使用,避免 Spring 误判默认注入入口。
      */
     AppWechatAuthClient(AppWechatAuthProperties properties,
                         ObjectMapper objectMapper,
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppHomeBannerController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppHomeBannerController.java
new file mode 100644
index 0000000..8209852
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppHomeBannerController.java
@@ -0,0 +1,37 @@
+package com.tcm.training.admin.app.home.controller;
+
+import com.tcm.training.admin.app.home.service.AppHomeBannerService;
+import com.tcm.training.admin.app.home.vo.AppHomeBannerVO;
+import com.tcm.training.admin.common.ApiResponse;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * APP 首页 banner 接口。
+ */
+@Validated
+@RestController
+@RequestMapping({"/api/app/home", "/api/v1/app/home"})
+public class AppHomeBannerController {
+
+    private final AppHomeBannerService appHomeBannerService;
+
+    /**
+     * 注入 APP 首页 banner 服务。
+     */
+    public AppHomeBannerController(AppHomeBannerService appHomeBannerService) {
+        this.appHomeBannerService = appHomeBannerService;
+    }
+
+    /**
+     * 获取 APP 首页顶部 banner 列表。
+     */
+    @GetMapping("/banners")
+    public ApiResponse<List<AppHomeBannerVO>> listBanners() {
+        return ApiResponse.ok(appHomeBannerService.listEnabledBanners());
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppHomeBannerService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppHomeBannerService.java
new file mode 100644
index 0000000..88b756f
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppHomeBannerService.java
@@ -0,0 +1,59 @@
+package com.tcm.training.admin.app.home.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.tcm.training.admin.app.home.vo.AppHomeBannerVO;
+import com.tcm.training.admin.system.entity.SystemBannerConfig;
+import com.tcm.training.admin.system.mapper.SystemBannerConfigMapper;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * APP 首页 banner 服务。
+ */
+@Service
+public class AppHomeBannerService {
+
+    private final SystemBannerConfigMapper systemBannerConfigMapper;
+
+    /**
+     * 注入 banner 配置查询能力。
+     */
+    public AppHomeBannerService(SystemBannerConfigMapper systemBannerConfigMapper) {
+        this.systemBannerConfigMapper = systemBannerConfigMapper;
+    }
+
+    /**
+     * 查询 APP 首页可展示的 banner 列表。
+     */
+    public List<AppHomeBannerVO> listEnabledBanners() {
+        LocalDateTime now = LocalDateTime.now();
+        List<SystemBannerConfig> records = systemBannerConfigMapper.selectList(new LambdaQueryWrapper<SystemBannerConfig>()
+                .eq(SystemBannerConfig::getDeleted, 0)
+                .eq(SystemBannerConfig::getPublishEnabled, 1)
+                .le(SystemBannerConfig::getStartAt, now)
+                .ge(SystemBannerConfig::getEndAt, now)
+                .orderByDesc(SystemBannerConfig::getId));
+        List<AppHomeBannerVO> result = new ArrayList<AppHomeBannerVO>(records.size());
+        for (SystemBannerConfig record : records) {
+            result.add(toBannerVO(record));
+        }
+        return result;
+    }
+
+    /**
+     * 将后台 banner 配置转换为 APP 首页展示对象。
+     */
+    private AppHomeBannerVO toBannerVO(SystemBannerConfig record) {
+        AppHomeBannerVO vo = new AppHomeBannerVO();
+        vo.setId(record.getId());
+        vo.setImageUrl(record.getBannerImg());
+        vo.setTitle(record.getTitle());
+        vo.setJumpType(record.getJumpType());
+        vo.setTargetId(record.getTargetId());
+        vo.setTargetUrl(record.getTargetUrl());
+        return vo;
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppHomeBannerVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppHomeBannerVO.java
new file mode 100644
index 0000000..a332a36
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppHomeBannerVO.java
@@ -0,0 +1,24 @@
+package com.tcm.training.admin.app.home.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * APP 首页顶部 banner 响应。
+ */
+@Getter
+@Setter
+public class AppHomeBannerVO {
+
+    private Long id;
+
+    private String imageUrl;
+
+    private String title;
+
+    private String jumpType;
+
+    private Long targetId;
+
+    private String targetUrl;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/statistics/controller/AppUserEntryLogController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/statistics/controller/AppUserEntryLogController.java
index e67879d..1b61d6d 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/statistics/controller/AppUserEntryLogController.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/statistics/controller/AppUserEntryLogController.java
@@ -1,6 +1,6 @@
 package com.tcm.training.admin.app.statistics.controller;
 
-import com.tcm.training.admin.app.auth.support.StpAppUserUtil;
+import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
 import com.tcm.training.admin.common.ApiResponse;
 import com.tcm.training.admin.statistics.service.AppUserEntryLogAppService;
 import org.springframework.validation.annotation.Validated;
@@ -17,20 +17,26 @@
 public class AppUserEntryLogController {
 
     private final AppUserEntryLogAppService appUserEntryLogAppService;
+    private final AppAuthSessionService appAuthSessionService;
 
     /**
      * 注入 APP 用户进入记录服务。
      */
-    public AppUserEntryLogController(AppUserEntryLogAppService appUserEntryLogAppService) {
+    public AppUserEntryLogController(AppUserEntryLogAppService appUserEntryLogAppService,
+                                     AppAuthSessionService appAuthSessionService) {
         this.appUserEntryLogAppService = appUserEntryLogAppService;
+        this.appAuthSessionService = appAuthSessionService;
     }
 
     /**
-     * 记录 APP 用户进入行为。
+     * 记录 APP 用户进入行为,游客访问时静默跳过。
      */
     @PostMapping("/user-entry-logs")
     public ApiResponse<Void> record() {
-        appUserEntryLogAppService.record(StpAppUserUtil.getLoginIdAsLong());
+        if (!appAuthSessionService.isLogin()) {
+            return ApiResponse.ok(null);
+        }
+        appUserEntryLogAppService.record(appAuthSessionService.getLoginUserId());
         return ApiResponse.ok(null);
     }
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserFeedbackController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserFeedbackController.java
index 49b8929..4ff86f9 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserFeedbackController.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserFeedbackController.java
@@ -1,6 +1,6 @@
 package com.tcm.training.admin.app.user.controller;
 
-import com.tcm.training.admin.app.auth.support.StpAppUserUtil;
+import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
 import com.tcm.training.admin.common.ApiResponse;
 import com.tcm.training.admin.system.dto.UserFeedbackSubmitRequest;
 import com.tcm.training.admin.system.service.UserFeedbackAppService;
@@ -21,20 +21,23 @@
 public class AppUserFeedbackController {
 
     private final UserFeedbackAppService userFeedbackAppService;
+    private final AppAuthSessionService appAuthSessionService;
 
     /**
      * 注入 APP 用户反馈服务。
      */
-    public AppUserFeedbackController(UserFeedbackAppService userFeedbackAppService) {
+    public AppUserFeedbackController(UserFeedbackAppService userFeedbackAppService,
+                                     AppAuthSessionService appAuthSessionService) {
         this.userFeedbackAppService = userFeedbackAppService;
+        this.appAuthSessionService = appAuthSessionService;
     }
 
     /**
-     * 提交 APP 用户意见反馈。
+     * 提交 APP 用户意见反馈,游客访问时按全局规则返回 401。
      */
     @PostMapping("/user-feedbacks")
     public ApiResponse<Void> submit(@Valid @RequestBody UserFeedbackSubmitRequest request) {
-        userFeedbackAppService.submit(StpAppUserUtil.getLoginIdAsLong(), request);
+        userFeedbackAppService.submit(appAuthSessionService.requireLoginUserId(), request);
         return ApiResponse.ok(null);
     }
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
index 3f62ad7..1fcff2c 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
@@ -52,10 +52,7 @@
         }
         String requestPath = SaHolder.getRequest().getRequestPath();
         if (isAppApi(requestPath)) {
-            if (isPublicAppApi(requestPath)) {
-                return;
-            }
-            StpAppUserUtil.checkLogin();
+            // APP 端允许游客访问,只有真正需要用户身份的接口才在业务层显式要求登录。
             return;
         }
         if (isPublicAdminApi(requestPath)) {
@@ -69,18 +66,6 @@
      */
     private boolean isAppApi(String requestPath) {
         return requestPath.startsWith("/api/app/") || requestPath.startsWith("/api/v1/app/");
-    }
-
-    /**
-     * 判断当前 APP 路由是否允许匿名访问。
-     */
-    private boolean isPublicAppApi(String requestPath) {
-        return requestPath.startsWith("/api/app/auth/")
-                || requestPath.startsWith("/api/v1/app/auth/")
-                || requestPath.startsWith("/api/app/register/")
-                || requestPath.startsWith("/api/v1/app/register/")
-                || "/api/app/version/check".equals(requestPath)
-                || "/api/v1/app/version/check".equals(requestPath);
     }
 
     /**
diff --git a/traditionalApp/pages/home/home.vue b/traditionalApp/pages/home/home.vue
index b0c1030..96a4c94 100644
--- a/traditionalApp/pages/home/home.vue
+++ b/traditionalApp/pages/home/home.vue
@@ -26,17 +26,13 @@
 			</view>
 
 			<!-- Banners -->
-			<view class="banner-section">
-				<view class="banner-main">
-					<image src="/static/image/home/live.png" mode="aspectFill" class="banner-img"></image>
-					<!-- Carousel dots -->
-					<view class="dots">
-						<view class="dot active"></view>
-						<image src="/static/image/home/circle.png" class="dot-img"></image>
-						<image src="/static/image/home/circle.png" class="dot-img"></image>
-					</view>
+			<view v-if="bannerList.length" class="banner-section">
+				<u-swiper :list="bannerList" :height="280" mode="none" name="imageUrl" bgColor="#fff" border-radius="20"
+					:interval="3000" @change="handleBannerChange" @click="handleBannerClick"></u-swiper>
+				<view class="dots">
+					<view v-for="(item, index) in bannerList" :key="item.id || index" class="dot"
+						:class="{ active: bannerActiveIndex === index }"></view>
 				</view>
-				<image src="/static/image/home/AI.png" mode="aspectFill" class="banner-sub"></image>
 			</view>
 
 			<!-- Live Section -->
@@ -134,13 +130,28 @@
 
 <script setup lang="ts">
 	import UpdatePopup from "@/components/updatePopup.vue";
-	import { ref, onMounted, computed, } from "vue";
-	import { onPageScroll } from "@dcloudio/uni-app";
+	import { ref, onMounted, computed } from "vue";
+	import { onPageScroll, onShow } from "@dcloudio/uni-app";
+	import { getBannerList } from "./service";
+
+	interface HomeBannerItem {
+		id: number | string;
+		imageUrl: string;
+		title?: string;
+		jumpType?: string;
+		targetId?: number | string | null;
+		targetUrl?: string;
+	}
 
 	const statusBarHeight = ref(0);
-	const scrollY = ref(0); // Add scrollY reactive variable
-	const showUpdatePopup = ref(false); // Update popup visibility, set to true by default for immediate display
+	const scrollY = ref(0);
+	const showUpdatePopup = ref(false);
+	const bannerList = ref<HomeBannerItem[]>([]);
+	const bannerActiveIndex = ref(0);
 
+	/**
+	 * 初始化导航栏所需系统信息。
+	 */
 	onMounted(() => {
 		uni.getSystemInfo({
 			success: (res) => {
@@ -149,47 +160,159 @@
 		});
 	});
 
-	onPageScroll((e) => {
-		scrollY.value = e.scrollTop; // Update scrollY on page scroll
+	/**
+	 * 每次进入首页都刷新 banner,确保拿到后台最新上下架结果。
+	 */
+	onShow(() => {
+		loadBannerList();
 	});
 
+	/**
+	 * 监听页面滚动以驱动导航栏渐变。
+	 */
+	onPageScroll((e) => {
+		scrollY.value = e.scrollTop;
+	});
+
+	/**
+	 * 计算自定义导航栏占位高度。
+	 */
 	const navbarHeightPx = computed(() => {
-		const navContentHeightRpx = 88; // Height of nav-content in rpx
+		const navContentHeightRpx = 88;
 		const screenWidthPx = uni.getSystemInfoSync().screenWidth;
 		const navContentHeightPx = (navContentHeightRpx / 750) * screenWidthPx;
 		return statusBarHeight.value + navContentHeightPx;
 	});
 
+	/**
+	 * 根据滚动距离切换导航栏背景色。
+	 */
 	const navbarBackgroundColor = computed(() => {
-		// Change to white after scrolling down 50rpx
 		const thresholdRpx = 50;
 		const screenWidthPx = uni.getSystemInfoSync().screenWidth;
 		const thresholdPx = (thresholdRpx / 750) * screenWidthPx;
-		const color = scrollY.value > thresholdPx ? 'white' : 'transparent';
-
-		return color;
+		return scrollY.value > thresholdPx ? 'white' : 'transparent';
 	});
 
+	/**
+	 * 请求首页顶部 banner 列表。
+	 */
+	const loadBannerList = async () => {
+		try {
+			const res = await getBannerList();
+			if (res?.code !== 0 || !Array.isArray(res.data)) {
+				bannerList.value = [];
+				bannerActiveIndex.value = 0;
+				return;
+			}
+			bannerList.value = res.data.filter((item: HomeBannerItem) => !!item?.imageUrl);
+			bannerActiveIndex.value = 0;
+		} catch {
+			bannerList.value = [];
+			bannerActiveIndex.value = 0;
+		}
+	};
+
+	/**
+	 * 同步当前轮播索引,用于自定义指示器展示。
+	 */
+	const handleBannerChange = (index: number) => {
+		bannerActiveIndex.value = index;
+	};
+
+	/**
+	 * 根据 banner 跳转类型分发到现有页面。
+	 */
+	const handleBannerClick = (index: number) => {
+		const currentBanner = bannerList.value[index];
+		if (!currentBanner) {
+			return;
+		}
+
+		// 课程和直播必须携带目标主键,避免跳进空白详情页。
+		if (currentBanner.jumpType === 'HEALTH_COURSE' || currentBanner.jumpType === 'LIVE_COURSE') {
+			if (!currentBanner.targetId) {
+				return;
+			}
+		}
+
+		if (currentBanner.jumpType === 'HEALTH_COURSE') {
+			uni.navigateTo({
+				url: `/pages/courseDetail/courseDetail?id=${currentBanner.targetId}`
+			});
+			return;
+		}
+
+		if (currentBanner.jumpType === 'LIVE_COURSE') {
+			uni.navigateTo({
+				url: `/pages/liveDetail/liveDetail?id=${currentBanner.targetId}`
+			});
+			return;
+		}
+
+		if (currentBanner.jumpType === 'MEMBER') {
+			uni.navigateTo({
+				url: '/pages/vip/vip'
+			});
+			return;
+		}
+
+		if (currentBanner.jumpType === 'THIRD_PARTY_URL') {
+			openThirdPartyUrl(currentBanner.targetUrl);
+		}
+	};
+
+	/**
+	 * 打开第三方链接。
+	 */
+	const openThirdPartyUrl = (targetUrl?: string) => {
+		if (!targetUrl) {
+			return;
+		}
+		// APP 端已适配第三方链接时,直接交由系统浏览器打开。
+		// #ifdef APP-PLUS
+		plus.runtime.openURL(targetUrl);
+		return;
+		// #endif
+		// #ifdef H5
+		window.open(targetUrl, '_blank');
+		return;
+		// #endif
+	};
+
+	/**
+	 * 打开更新弹窗。
+	 */
 	const handleOpenUpdate = () => {
 		showUpdatePopup.value = true;
 	};
 
+	/**
+	 * 关闭更新弹窗。
+	 */
 	const closeUpdatePopup = () => {
 		showUpdatePopup.value = false;
 	};
 
+	/**
+	 * 确认更新后关闭弹窗。
+	 */
 	const handleUpdateConfirm = () => {
-		console.log('User confirmed update');
-		// Handle update logic here
 		closeUpdatePopup();
 	};
 
+	/**
+	 * 跳转直播详情页。
+	 */
 	const goLiveDetail = () => {
 		uni.navigateTo({
 			url: '/pages/liveDetail/liveDetail'
 		});
 	};
 
+	/**
+	 * 跳转搜索页。
+	 */
 	const goSearch = () => {
 		uni.navigateTo({
 			url: '/pages/search/search'
@@ -320,50 +443,31 @@
 	.banner-section {
 		margin: 0 24rpx;
 		margin-top: 32rpx;
+		position: relative;
 
-		.banner-main {
-			position: relative;
-			width: 702rpx;
-			height: 280rpx;
-			border-radius: 20rpx;
-			overflow: hidden;
+		.dots {
+			position: absolute;
+			bottom: 16rpx;
+			left: 0;
+			width: 100%;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			pointer-events: none;
 
-			.banner-img {
-				width: 100%;
-				height: 100%;
+			.dot {
+				width: 10rpx;
+				height: 10rpx;
+				background: rgba(255, 255, 255, 0.5);
+				border-radius: 10rpx;
+				margin: 0 6rpx;
+				transition: all 0.3s ease;
 			}
 
-			.dots {
-				position: absolute;
-				bottom: 16rpx;
-				left: 0;
-				width: 100%;
-				display: flex;
-				justify-content: center;
-				align-items: center;
-
-				.dot {
-					width: 24rpx;
-					height: 10rpx;
-					background: #FFF;
-					border-radius: 6rpx;
-					margin: 0 6rpx;
-				}
-
-				.dot-img {
-					width: 10rpx;
-					height: 10rpx;
-					margin: 0 6rpx;
-					opacity: 0.5;
-				}
+			.dot.active {
+				width: 24rpx;
+				background: #FFF;
 			}
-		}
-
-		.banner-sub {
-			width: 702rpx;
-			height: 160rpx;
-			margin-top: 20rpx;
-			border-radius: 16rpx;
 		}
 	}
 
diff --git a/traditionalApp/pages/home/service.js b/traditionalApp/pages/home/service.js
index d30e618..87d0b69 100644
--- a/traditionalApp/pages/home/service.js
+++ b/traditionalApp/pages/home/service.js
@@ -1,17 +1,22 @@
 import request from '@/utils/request.js'
 
-//首页模块--轮播图
-export const getBannerList = (params) => {
-	return request.get(`/index/getBannerList`, params, 1)
+/**
+ * 获取 APP 首页顶部 Banner 列表。
+ */
+export const getBannerList = () => {
+	return request.get('/api/app/home/banners', {}, false, false)
 }
 
-
-//获取攻略列表
+/**
+ * 获取首页攻略列表。
+ */
 export const getGuideList = (params) => {
 	return request.get(`/index/getGuideList`, params, 1)
 }
 
-//获取公告列表
+/**
+ * 获取首页公告列表。
+ */
 export const getNoticeList = (params) => {
 	return request.get(`/index/getNoticeList`, params, 1)
-}
\ No newline at end of file
+}
diff --git a/traditionalApp/utils/request.js b/traditionalApp/utils/request.js
index 3a53ec7..4fe0608 100644
--- a/traditionalApp/utils/request.js
+++ b/traditionalApp/utils/request.js
@@ -8,6 +8,7 @@
 
 const waitingList = [] //等待队列
 const excutingList = [] //执行队列
+let appAuthRedirecting = false
 
 /**
  * 判断当前响应码是否属于成功态。
@@ -80,11 +81,7 @@
 					return
 				}
 				if (payload.code == 401 || payload.code == 510 || payload.code == 504 || payload.code == 505) {
-					// uni.clearStorageSync()
-					// handleLogout('登录过期,请重新登录')
-					// uni.navigateTo({
-					// 	url: '/pages/login/login'
-					// })
+					handleUnauthorized(payload)
 					resolve(payload)
 					return
 				}
@@ -135,29 +132,51 @@
 }
 
 /**
- * 被冻结时跳转到登录页并取消后续请求。
+ * 收到未登录响应时清理 APP 登录态并跳转登录页。
  */
-function handleLogout(str) {
-	let routeStr = getCurrentPages()[getCurrentPages().length - 1].route
-	if (uni.getStorageSync('showLogoutModal') == 1) {
-		uni.setStorageSync('showLogoutModal', 0)
-		uni.showModal({
-			title: '提示',
-			content: str,
-			showCancel: false,
-			success: function(res) {
-				if (res.confirm) {
-					if (routeStr == 'pages/login/login') {
-						return
-					}
-					uni.reLaunch({
-						url: '/pages/login/login'
-					})
-				}
-			}
-		});
-		clearAllRequest()
+function handleUnauthorized(payload) {
+	const routeStr = getCurrentRoute()
+	clearLoginSession()
+	clearAllRequest()
+	if (routeStr === 'pages/login/login' || appAuthRedirecting) {
+		return
 	}
+	appAuthRedirecting = true
+	uni.showToast({
+		title: resolveMessage(payload) || '请先登录',
+		duration: 1500,
+		icon: 'none',
+		mask: true,
+	})
+	setTimeout(() => {
+		uni.reLaunch({
+			url: '/pages/login/login'
+		})
+		setTimeout(() => {
+			appAuthRedirecting = false
+		}, 300)
+	}, 150)
+}
+
+/**
+ * 清除 APP 登录态缓存。
+ */
+function clearLoginSession() {
+	uni.removeStorageSync('token')
+	uni.removeStorageSync('appUserInfo')
+	uni.removeStorageSync('loginRedirectMeta')
+	uni.setStorageSync('showLogoutModal', 0)
+}
+
+/**
+ * 获取当前页面路由,便于避免在登录页重复跳转。
+ */
+function getCurrentRoute() {
+	const currentPages = getCurrentPages()
+	if (!currentPages || currentPages.length === 0) {
+		return ''
+	}
+	return currentPages[currentPages.length - 1].route || ''
 }
 
 /**

--
Gitblit v1.7.1