hejianhao
2026-08-20 419b25feff8eac325c154efc8f3f6e4b11700483
feat0806: 动态生成微赞URL
2个文件已修改
85 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/mine/mine.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java
@@ -5,7 +5,13 @@
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.tcm.training.admin.agent.entity.ProvincialAgent;
import com.tcm.training.admin.agent.mapper.ProvincialAgentMapper;
import com.tcm.training.admin.app.home.vo.AppVzanLiveEntryVO;
import com.tcm.training.admin.app.user.service.AppUserCenterService;
import com.tcm.training.admin.app.user.vo.AppUserCenterVO;
import com.tcm.training.admin.live.entity.Clinic;
import com.tcm.training.admin.live.mapper.ClinicMapper;
import com.tcm.training.admin.live.service.LiveRoomSettingService;
import com.tcm.training.admin.live.vo.LiveRoomSettingVO;
import lombok.extern.slf4j.Slf4j;
@@ -18,37 +24,37 @@
@Slf4j
@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 = "18912345680";
    private static final String VZAN_NICKNAME = "微赞测试用户";
    private static final String VZAN_AVATAR = "https://static3.njyqkj0ksyz.com/zhibo/images/defaultuser.jpg";
    private final LiveRoomSettingService liveRoomSettingService;
    private final AppUserCenterService appUserCenterService;
    private final ClinicMapper clinicMapper;
    private final ProvincialAgentMapper provincialAgentMapper;
    public AppVzanLiveEntryService(LiveRoomSettingService liveRoomSettingService) {
    public AppVzanLiveEntryService(LiveRoomSettingService liveRoomSettingService,
                                   AppUserCenterService appUserCenterService,
                                   ClinicMapper clinicMapper,
                                   ProvincialAgentMapper provincialAgentMapper) {
        this.liveRoomSettingService = liveRoomSettingService;
        this.appUserCenterService = appUserCenterService;
        this.clinicMapper = clinicMapper;
        this.provincialAgentMapper = provincialAgentMapper;
    }
    public AppVzanLiveEntryVO createEntry() {
        validateStaticParameters();
        LiveRoomSettingVO setting = liveRoomSettingService.current();
        String liveRoomUrl = setting == null ? null : StrUtil.trim(setting.getLiveRoomUrl());
        validateLiveRoomUrl(liveRoomUrl);
        AppUserCenterVO user = appUserCenterService.getCurrentUserCenter();
        String followPhone = resolveFollowPhone(user.getClinicId());
        JSONObject payload = new JSONObject();
        payload.set("tuid", VZAN_TUID);
        payload.set("tuid", user.getId());
        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);
        }
        log.info("请求参数:{}....{}", JSONUtil.toJsonStr(payload), payload.toString());
        payload.set("avatar", StrUtil.trim(user.getAvatar()));
        log.info("微赞入口参数已生成,tuid={}, third_ts={}", user.getId(), payload.get("third_ts"));
        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)
@@ -60,13 +66,20 @@
        return new AppVzanLiveEntryVO(entryUrl);
    }
    private void validateStaticParameters() {
        if (VZAN_AUTH_SECRET.getBytes(StandardCharsets.UTF_8).length != 16) {
            throw new IllegalStateException("微赞鉴权密钥必须为16字节");
    private String resolveFollowPhone(Long clinicId) {
        if (clinicId == null) {
            throw new IllegalArgumentException("当前用户未绑定诊所");
        }
        if (StrUtil.isBlank(VZAN_TUID)) {
            throw new IllegalStateException("微赞用户标识暂未配置");
        Clinic clinic = clinicMapper.selectById(clinicId);
        if (clinic == null || clinic.getAgentId() == null) {
            throw new IllegalArgumentException("当前诊所未绑定代理");
        }
        ProvincialAgent agent = provincialAgentMapper.selectById(clinic.getAgentId());
        String contactInfo = agent == null ? null : StrUtil.trim(agent.getContactInfo());
        if (StrUtil.isBlank(contactInfo)) {
            throw new IllegalArgumentException("所属代理未配置联系电话");
        }
        return contactInfo;
    }
    private void validateLiveRoomUrl(String liveRoomUrl) {
@@ -81,10 +94,5 @@
        } catch (IllegalArgumentException ex) {
            throw new IllegalArgumentException("直播间地址必须为有效的HTTPS地址", ex);
        }
    }
    public static void main(String[] args) {
        long l = System.currentTimeMillis() + 5000;
        log.info("l:{}", l);
    }
}
traditionalApp/pages/mine/mine.vue
@@ -55,6 +55,7 @@
import { onLoad, onShow } from '@dcloudio/uni-app'
import { getNoticeList } from '../notice/service'
import { getUserCenter } from './service'
import { createLiveRoomEntry } from '../home/service'
import { getRuntimeVersionName } from '@/utils/app-version.js'
import { hasStoredSession } from '@/utils/auth-session.js'
import manifestConfig from '../../manifest.json'
@@ -95,6 +96,7 @@
    url: string
    iconClass?: string
    requiresLogin?: boolean
    dynamicVzan?: boolean
}
interface NoticeSummaryItem {
@@ -161,9 +163,9 @@
        { title: '帮助中心', icon: '/static/image/mine/help.png', url: '/pages/helpCenter/helpCenter', iconClass: 'icon-help' },
        { 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 liveOrderItem = { title: '直播订单', icon: '', url: '', iconClass: 'icon-live-order', requiresLogin: true, dynamicVzan: true }
    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 }]
        ? [{ title: '直播统计', icon: '', url: '', iconClass: 'icon-live-stats', requiresLogin: true, dynamicVzan: true }]
        : []
    const agreementList = [
        { title: '用户协议', icon: '/static/image/mine/coustom.png', url: '/pages/protocol/protocol?type=user', iconClass: 'icon-user-agree' },
@@ -386,7 +388,24 @@
/**
 * 统一跳转个人中心相关页面。
 */
function goPage(target: string | MenuItem) {
async function goPage(target: string | MenuItem) {
    if (typeof target !== 'string' && target.dynamicVzan) {
        try {
            uni.showLoading({ title: '正在进入', mask: true })
            const response = await createLiveRoomEntry()
            const targetUrl = response?.code === 0 ? response.data?.url : ''
            if (!targetUrl) {
                uni.showToast({ title: response?.msg || '获取跳转地址失败', icon: 'none' })
                return
            }
            uni.navigateTo({ url: buildWebviewUrl(targetUrl, target.title) })
        } catch {
            uni.showToast({ title: '获取跳转地址失败', icon: 'none' })
        } finally {
            uni.hideLoading()
        }
        return
    }
    const url = typeof target === 'string' ? target : target.url
    if (!url) {
        return