From 6e873c1820bf2a0560d09ea6f96ddf963d71024f Mon Sep 17 00:00:00 2001
From: hejianhao <15708179461@qq.com>
Date: 星期三, 19 八月 2026 17:55:16 +0800
Subject: [PATCH] feat0806: 拼接跳转地址

---
 traditionalApp/pages/home/home.vue                                                                                    |   40 +++++++++++--
 traditionalApp/pages/mine/mine.vue                                                                                    |   10 +-
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java         |   81 +++++++++++++++++++++++++++
 traditionalApp/pages/home/service.js                                                                                  |    9 ++
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppLiveRoomSettingController.java |   15 ++++
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppVzanLiveEntryVO.java                   |   10 +++
 6 files changed, 151 insertions(+), 14 deletions(-)

diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppLiveRoomSettingController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppLiveRoomSettingController.java
index e84d27c..4af26a0 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppLiveRoomSettingController.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppLiveRoomSettingController.java
@@ -1,9 +1,12 @@
 package com.tcm.training.admin.app.home.controller;
 
+import com.tcm.training.admin.app.home.service.AppVzanLiveEntryService;
+import com.tcm.training.admin.app.home.vo.AppVzanLiveEntryVO;
 import com.tcm.training.admin.common.ApiResponse;
 import com.tcm.training.admin.live.service.LiveRoomSettingService;
 import com.tcm.training.admin.live.vo.LiveRoomSettingVO;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -11,13 +14,21 @@
 @RequestMapping({"/api/app/home", "/api/v1/app/home"})
 public class AppLiveRoomSettingController {
     private final LiveRoomSettingService service;
+    private final AppVzanLiveEntryService vzanLiveEntryService;
 
-    public AppLiveRoomSettingController(LiveRoomSettingService service) {
+    public AppLiveRoomSettingController(LiveRoomSettingService service,
+                                        AppVzanLiveEntryService vzanLiveEntryService) {
         this.service = service;
+        this.vzanLiveEntryService = vzanLiveEntryService;
     }
 
     @GetMapping("/live-room-setting")
     public ApiResponse<LiveRoomSettingVO> current() {
         return ApiResponse.ok(service.current());
     }
-}
\ No newline at end of file
+
+    @PostMapping("/live-room-entry")
+    public ApiResponse<AppVzanLiveEntryVO> createEntry() {
+        return ApiResponse.ok(vzanLiveEntryService.createEntry());
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java
new file mode 100644
index 0000000..e8f1c7a
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java
@@ -0,0 +1,81 @@
+package com.tcm.training.admin.app.home.service;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SmUtil;
+import cn.hutool.crypto.symmetric.SymmetricCrypto;
+import cn.hutool.json.JSONObject;
+import com.tcm.training.admin.app.home.vo.AppVzanLiveEntryVO;
+import com.tcm.training.admin.live.service.LiveRoomSettingService;
+import com.tcm.training.admin.live.vo.LiveRoomSettingVO;
+import org.springframework.stereotype.Service;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+@Service
+public class AppVzanLiveEntryService {
+    // TODO 以下参数由微赞提供,联调前替换;正式环境应迁移至环境变量或配置中心。
+//    private static final String VZAN_AUTH_SECRET = "e70ccf6a-1745-42b4-be41-aaed6f65700f";
+    private static final String VZAN_AUTH_SECRET = "2b4fefe91d8cba50";
+    private static final String VZAN_TUID = "9594";
+    private static final String followPhone = "15831021084";
+    private static final String VZAN_NICKNAME = "微赞测试用户";
+    private static final String VZAN_AVATAR = "https://static3.njyqkj0ksyz.com/zhibo/images/defaultuser.jpg";
+
+    private final LiveRoomSettingService liveRoomSettingService;
+
+    public AppVzanLiveEntryService(LiveRoomSettingService liveRoomSettingService) {
+        this.liveRoomSettingService = liveRoomSettingService;
+    }
+
+    public AppVzanLiveEntryVO createEntry() {
+        validateStaticParameters();
+        LiveRoomSettingVO setting = liveRoomSettingService.current();
+        String liveRoomUrl = setting == null ? null : StrUtil.trim(setting.getLiveRoomUrl());
+        validateLiveRoomUrl(liveRoomUrl);
+
+        JSONObject payload = new JSONObject();
+        payload.set("tuid", VZAN_TUID);
+        payload.set("followPhone", followPhone);
+        payload.set("third_ts", System.currentTimeMillis());
+        if (StrUtil.isNotBlank(VZAN_NICKNAME)) {
+            payload.set("nickname", VZAN_NICKNAME);
+        }
+        if (StrUtil.isNotBlank(VZAN_AVATAR)) {
+            payload.set("avatar", VZAN_AVATAR);
+        }
+
+        SymmetricCrypto sm4 = SmUtil.sm4(VZAN_AUTH_SECRET.getBytes(StandardCharsets.UTF_8));
+        String encryptedData = sm4.encryptBase64(payload.toString(), StandardCharsets.UTF_8);
+        String entryUrl = UriComponentsBuilder.fromHttpUrl(liveRoomUrl)
+                .replaceQueryParam("data", encryptedData)
+                .build()
+                .encode(StandardCharsets.UTF_8)
+                .toUriString();
+        return new AppVzanLiveEntryVO(entryUrl);
+    }
+
+    private void validateStaticParameters() {
+        if (VZAN_AUTH_SECRET.getBytes(StandardCharsets.UTF_8).length != 16) {
+            throw new IllegalStateException("微赞鉴权密钥必须为16字节");
+        }
+        if (StrUtil.isBlank(VZAN_TUID)) {
+            throw new IllegalStateException("微赞用户标识暂未配置");
+        }
+    }
+
+    private void validateLiveRoomUrl(String liveRoomUrl) {
+        if (StrUtil.isBlank(liveRoomUrl)) {
+            throw new IllegalArgumentException("直播间地址暂未配置");
+        }
+        try {
+            URI uri = URI.create(liveRoomUrl);
+            if (!"https".equalsIgnoreCase(uri.getScheme()) || StrUtil.isBlank(uri.getHost())) {
+                throw new IllegalArgumentException("直播间地址必须为有效的HTTPS地址");
+            }
+        } catch (IllegalArgumentException ex) {
+            throw new IllegalArgumentException("直播间地址必须为有效的HTTPS地址", ex);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppVzanLiveEntryVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppVzanLiveEntryVO.java
new file mode 100644
index 0000000..660cb3c
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppVzanLiveEntryVO.java
@@ -0,0 +1,10 @@
+package com.tcm.training.admin.app.home.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class AppVzanLiveEntryVO {
+    private final String url;
+}
\ No newline at end of file
diff --git a/traditionalApp/pages/home/home.vue b/traditionalApp/pages/home/home.vue
index 81f37c4..4ff4341 100644
--- a/traditionalApp/pages/home/home.vue
+++ b/traditionalApp/pages/home/home.vue
@@ -141,7 +141,7 @@
 <script setup lang="ts">
 	import { computed, ref, onMounted } from "vue";
 	import { onPullDownRefresh, onShow } from "@dcloudio/uni-app";
-	import { getBannerList, getHomeFeaturedContent, getHomeLiveCourseList, getLiveRoomSetting } from "./service";
+	import { createLiveRoomEntry, getBannerList, getHomeFeaturedContent, getHomeLiveCourseList, getLiveRoomSetting } from "./service";
 
 	interface HomeBannerItem {
 		id : number | string;
@@ -203,6 +203,7 @@
 	});
 	const weiZanLiveH5Url = ref('');
 	const weiZanLiveBanner = ref('');
+	const weiZanLiveOpening = ref(false);
 
 	const operatorTrainingLiveCourse = computed(() => {
 		const list = liveCourseList.value;
@@ -330,14 +331,41 @@
 	/**
 	 * 跳转后台配置的微赞直播 H5 页面。
 	 */
-	const goWeiZanLive = () => {
-		if (!weiZanLiveH5Url.value) {
+	const goWeiZanLive = async () => {
+		if (!weiZanLiveH5Url.value.trim()) {
 			uni.showToast({ title: '直播间地址暂未配置', icon: 'none' });
 			return;
 		}
-		uni.navigateTo({
-			url: `/pages/webview/webview?url=${encodeURIComponent(weiZanLiveH5Url.value)}&title=${encodeURIComponent('微赞直播')}`
-		});
+		if (weiZanLiveOpening.value) {
+			return;
+		}
+		weiZanLiveOpening.value = true;
+		uni.showLoading({ title: '正在进入直播间', mask: true });
+		try {
+			const res = await createLiveRoomEntry();
+			const targetUrl = res?.data?.url?.trim();
+			if (res?.code !== 0 || !targetUrl || !isValidLiveRoomUrl(targetUrl)) {
+				uni.showToast({ title: res?.msg || '获取直播间地址失败', icon: 'none' });
+				return;
+			}
+			uni.navigateTo({
+				url: `/pages/webview/webview?url=${encodeURIComponent(targetUrl)}&title=${encodeURIComponent('微赞直播')}`
+			});
+		} catch {
+			uni.showToast({ title: '获取直播间地址失败', icon: 'none' });
+		} finally {
+			uni.hideLoading();
+			weiZanLiveOpening.value = false;
+		}
+	};
+
+	const isValidLiveRoomUrl = (targetUrl : string) => {
+		try {
+			const parsedUrl = new URL(targetUrl);
+			return parsedUrl.protocol === 'https:';
+		} catch {
+			return false;
+		}
 	};
 
 	/**
diff --git a/traditionalApp/pages/home/service.js b/traditionalApp/pages/home/service.js
index 1a3851e..d99b35d 100644
--- a/traditionalApp/pages/home/service.js
+++ b/traditionalApp/pages/home/service.js
@@ -47,4 +47,11 @@
  */
 export const getLiveRoomSetting = () => {
 	return publicGet('/api/app/home/live-room-setting')
-}
\ No newline at end of file
+}
+
+/**
+ * 根据当前登录用户生成短时有效的微赞直播入口。
+ */
+export const createLiveRoomEntry = () => {
+	return request.post('/api/app/home/live-room-entry', {}, false, false)
+}
diff --git a/traditionalApp/pages/mine/mine.vue b/traditionalApp/pages/mine/mine.vue
index e40d774..7b10500 100644
--- a/traditionalApp/pages/mine/mine.vue
+++ b/traditionalApp/pages/mine/mine.vue
@@ -162,17 +162,17 @@
 		{ title: '意见反馈', icon: '/static/image/mine/message.png', url: '/pages/feedback/feedback', iconClass: 'icon-feedback' },
 	]
 	const liveOrderItem = { title: '直播订单', icon: '/static/image/mine/dingdan.png', url: buildWebviewUrl(WEIZAN_LIVE_ORDER_URL, '直播订单'), iconClass: 'icon-live-order', requiresLogin: true }
-	const liveMenuList = [
-		{ title: '直播统计', icon: '/static/image/mine/tongji.png', url: buildWebviewUrl(WEIZAN_LIVE_STATS_URL, '直播统计'), iconClass: 'icon-live-stats', requiresLogin: true },
-		liveOrderItem,
-	]
+	const liveStatsList = Number(userCenter.value.type) === 1
+		? [{ title: '直播统计', icon: '/static/image/mine/tongji.png', url: buildWebviewUrl(WEIZAN_LIVE_STATS_URL, '直播统计'), iconClass: 'icon-live-stats', requiresLogin: true }]
+		: []
 	const agreementList = [
 		{ title: '用户协议', icon: '/static/image/mine/coustom.png', url: '/pages/protocol/protocol?type=user', iconClass: 'icon-user-agree' },
 		{ title: '隐私协议', icon: '/static/image/mine/agree.png', url: '/pages/protocol/protocol?type=privacy', iconClass: 'icon-privacy' },
 	]
 	if (userRole.value === 'doctor') {
 		return [
-			...liveMenuList,
+			...liveStatsList,
+			liveOrderItem,
 			...baseList,
 			{ title: '平台资质', icon: '/static/image/mine/chimaping.png', url: '/pages/qualification/qualification', iconClass: 'icon-platform' },
 			...agreementList,

--
Gitblit v1.7.1