| tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppLiveRoomSettingController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppVzanLiveEntryVO.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/pages/home/home.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/pages/home/service.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/pages/mine/mine.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
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()); } @PostMapping("/live-room-entry") public ApiResponse<AppVzanLiveEntryVO> createEntry() { return ApiResponse.ok(vzanLiveEntryService.createEntry()); } } tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java
New file @@ -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); } } } tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppVzanLiveEntryVO.java
New file @@ -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; } 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; } 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(weiZanLiveH5Url.value)}&title=${encodeURIComponent('微赞直播')}` 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; } }; /** traditionalApp/pages/home/service.js
@@ -48,3 +48,10 @@ export const getLiveRoomSetting = () => { return publicGet('/api/app/home/live-room-setting') } /** * 根据当前登录用户生成短时有效的微赞直播入口。 */ export const createLiveRoomEntry = () => { return request.post('/api/app/home/live-room-entry', {}, false, false) } 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,