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