hejianhao
2026-08-19 6e873c1820bf2a0560d09ea6f96ddf963d71024f
feat0806: 拼接跳转地址
2个文件已添加
4个文件已修改
157 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppLiveRoomSettingController.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppVzanLiveEntryVO.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/home/home.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/home/service.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/mine/mine.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | 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,