运营版uniapp多商户商城小程序/种草/公众号/h5/app/商家入驻/在线客服/秒杀/拼团/砍价
源码获取:ym7k.com/17882/



多商户商城直播系统:全终端观看与推流限制的工程突破
引子:直播的技术门槛不在播放,而在"全终端"
“全终端观看直播,H5 外所有终端支持推流”——这句话看起来只是一个功能描述,但它触及了多商户商城系统中最难啃的技术骨头。
直播的播放和推流,在不同终端上的技术路径完全不同。小程序有微信的推流组件限制,App 需要集成原生推流 SDK,H5 的直播播放依赖浏览器对 HLS/FLV 的支持程度,公众号受限于微信内置浏览器。要让一套系统在所有这些终端上都能正常观看,并且让商家在除 H5 外的所有终端上都能推流,需要解决一系列工程问题。
更重要的是,这是一个多商户系统。每个商户都需要独立的直播间、独立的推流地址、独立的商品列表、独立的订单归属。直播不是单商户的功能,而是多租户架构下的实时业务。
本文从多商户直播的架构设计、全终端播放的兼容方案、App 与小程序推流的技术实现、直播与商城的业务耦合,到直播的存储与回放,展开这类系统的完整技术分析。
一、多商户直播的架构设计
1.1 直播系统的三层结构
多商户直播系统的架构可以抽象为三层。
流媒体层。 负责音视频流的接收(推流)、转码、分发(拉流)。核心组件是 RTMP 服务器、转码服务和 CDN。
业务层。 负责直播间的创建、状态管理、商品关联、互动消息、订单归因。这是商城系统与流媒体层的交界面。
终端层。 负责推流和播放。不同终端使用不同的 SDK 和协议。
三层之间的数据流是:主播通过终端推流 → 流媒体层接收并转码 → 业务层记录直播间状态 → 观众通过终端拉流 → 业务层处理互动和订单。
1.2 多租户隔离
多商户系统要求每个商户的直播间完全隔离。隔离维度包括:推流地址隔离(每个商户不同的推流密钥)、播放地址隔离(不同的直播间不同的播放地址)、商品隔离(直播间只能挂本商户的商品)、订单隔离(直播订单归属于对应的商户)。
CREATE TABLE `live_room` ( `id` int(11) NOT NULL AUTO_INCREMENT, `merchant_id` int(11) NOT NULL COMMENT '所属商户', `title` varchar(200) NOT NULL, `cover` varchar(255) NOT NULL, `stream_key` varchar(64) NOT NULL COMMENT '推流密钥', `push_url` varchar(500) DEFAULT NULL COMMENT '推流地址', `play_url_flv` varchar(500) DEFAULT NULL COMMENT 'HTTP-FLV 播放地址', `play_url_hls` varchar(500) DEFAULT NULL COMMENT 'HLS 播放地址', `play_url_rtmp` varchar(500) DEFAULT NULL COMMENT 'RTMP 播放地址', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=预告 1=直播中 2=已结束', `scheduled_at` datetime DEFAULT NULL COMMENT '预告开播时间', `started_at` datetime DEFAULT NULL, `ended_at` datetime DEFAULT NULL, `viewer_count` int(11) NOT NULL DEFAULT '0', `peak_viewer` int(11) NOT NULL DEFAULT '0', `like_count` int(11) NOT NULL DEFAULT '0', `replay_url` varchar(500) DEFAULT NULL COMMENT '回放地址', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_stream_key` (`stream_key`), KEY `idx_merchant_status` (`merchant_id`, `status`), KEY `idx_status_scheduled` (`status`, `scheduled_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
stream_key 是推流的核心凭证。它是一个唯一字符串,主播推流时需要提供这个密钥,服务器据此识别是哪个直播间。merchant_id 保证了每个直播间归属于特定的商户。
1.3 流媒体服务器的选型
对于多商户商城的私有化部署,流媒体服务器的选择有三个方案。
方案一:SRS(Simple Realtime Server)。 开源、轻量、支持 RTMP 推流和 HLS/HTTP-FLV 拉流。部署简单,适合中小型平台。
方案二:Nginx-rtmp-module。 基于 Nginx 的 RTMP 模块,配置灵活,但功能相对基础,不支持 HTTP-FLV 的低延迟播放。
方案三:云直播服务。 阿里云直播、腾讯云直播、声网等。优点是稳定、低延迟、全球覆盖;缺点是成本随流量线性增长,数据不自主可控。
对于私有化部署的多商户商城,SRS 是最务实的选择。它支持 RTMP 推流、HLS/HTTP-FLV 拉流,且有完善的 HTTP API 支持直播间管理。
二、全终端播放的兼容方案
2.1 各终端的播放能力差异
直播播放的核心是协议和播放器的选择。不同终端对协议的支持程度不同。
微信小程序。 支持 live-player 组件播放 RTMP 和 HTTP-FLV,但需要开通小程序的直播权限(类目审核)。HLS 在小程序中延迟较高(10-30秒),HTTP-FLV 延迟较低(3-5秒)。
App。 可以使用原生播放器(如 ijkplayer、ExoPlayer)或 UniApp 的原生插件。支持 RTMP、HTTP-FLV、HLS 全协议。
H5。 浏览器的支持最受限。Safari 原生支持 HLS,Chrome 需要 hls.js 库。HTTP-FLV 需要 flv.js 库。RTMP 在浏览器中已不支持(Flash 已废弃)。
公众号。 本质上是 H5 运行在微信内置浏览器中,播放能力与 H5 相同。
2.2 播放协议的降级策略
由于各终端的支持程度不同,系统需要提供协议降级:优先使用低延迟协议(HTTP-FLV),不支持时降级到 HLS。
// utils/live/player.js /** * 根据终端和协议支持情况,选择合适的播放地址 */ export function selectPlayUrl(room) { // #ifdef MP-WEIXIN // 小程序优先使用 HTTP-FLV(低延迟) return room.play_url_flv || room.play_url_hls; // #endif // #ifdef APP-PLUS // App 优先使用 HTTP-FLV,也支持 RTMP return room.play_url_flv || room.play_url_rtmp || room.play_url_hls; // #endif // #ifdef H5 // H5 根据浏览器能力选择 if (supportsFLV()) { return room.play_url_flv; } if (supportsHLS()) { return room.play_url_hls; } return room.play_url_hls; // 兜底 // #endif } function supportsFLV() { // #ifdef H5 // 检查浏览器是否支持 MSE(Media Source Extensions) // flv.js 依赖 MSE return typeof window.MediaSource !== 'undefined' && window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E"'); // #endif return false; } function supportsHLS() { // #ifdef H5 const video = document.createElement('video'); return video.canPlayType('application/vnd.apple.mpegurl') !== ''; // #endif return false; }
2.3 H5 端的播放器实现
H5 端的直播播放是最复杂的,因为需要根据浏览器能力动态加载播放器。
<!-- components/LivePlayer.vue --> <template> <view class="live-player"> <!-- 小程序端使用 live-player 组件 --> <!-- #ifdef MP-WEIXIN --> <live-player :src="playUrl" mode="live" autoplay @statechange="onStateChange" @error="onError" /> <!-- #endif --> <!-- H5 端使用 video 标签 + 播放器库 --> <!-- #ifdef H5 --> <video ref="videoEl" class="h5-player" controls autoplay playsinline /> <!-- #endif --> <!-- App 端使用原生播放器 --> <!-- #ifdef APP-PLUS --> <view class="app-player"> <!-- App 端通过原生插件或 video 组件播放 --> <video :src="playUrl" autoplay controls /> </view> <!-- #endif --> </view> </template> <script> export default { props: { room: Object, }, data() { return { playUrl: '', flvPlayer: null, hlsPlayer: null, }; }, mounted() { this.playUrl = this.selectUrl(); this.initPlayer(); }, beforeDestroy() { this.destroyPlayer(); }, methods: { selectUrl() { // #ifdef MP-WEIXIN return this.room.play_url_flv || this.room.play_url_hls; // #endif // #ifdef APP-PLUS return this.room.play_url_flv || this.room.play_url_rtmp || this.room.play_url_hls; // #endif // #ifdef H5 if (this.supportsFLV() && this.room.play_url_flv) { return this.room.play_url_flv; } return this.room.play_url_hls; // #endif }, supportsFLV() { // #ifdef H5 return typeof window.MediaSource !== 'undefined'; // #endif return false; }, async initPlayer() { // #ifdef H5 await this.initH5Player(); // #endif }, async initH5Player() { const videoEl = this.$refs.videoEl; if (!videoEl) return; if (this.playUrl.includes('.flv')) { // 动态加载 flv.js const flvjs = await import('flv.js'); if (flvjs.default.isSupported()) { this.flvPlayer = flvjs.default.createPlayer({ type: 'flv', url: this.playUrl, isLive: true, hasAudio: true, }); this.flvPlayer.attachMediaElement(videoEl); this.flvPlayer.load(); this.flvPlayer.play(); } } else if (this.playUrl.includes('.m3u8')) { // 动态加载 hls.js(Safari 原生支持 HLS,不需要) if (!videoEl.canPlayType('application/vnd.apple.mpegurl')) { const Hls = await import('hls.js'); if (Hls.default.isSupported()) { this.hlsPlayer = new Hls.default(); this.hlsPlayer.loadSource(this.playUrl); this.hlsPlayer.attachMedia(videoEl); } } else { videoEl.src = this.playUrl; } } }, destroyPlayer() { if (this.flvPlayer) { this.flvPlayer.destroy(); this.flvPlayer = null; } if (this.hlsPlayer) { this.hlsPlayer.destroy(); this.hlsPlayer = null; } }, onStateChange(e) { // 小程序播放状态变化 }, onError(e) { console.error('播放错误', e); }, }, }; </script>
这个播放器组件的核心设计是动态加载播放器库 + 协议降级。H5 端根据播放地址的格式(.flv 或 .m3u8)动态加载 flv.js 或 hls.js,而不是在初始包中引入所有播放器。这降低了首屏加载时间。
三、推流:App 与小程序的技术实现
3.1 推流的技术门槛
推流比播放的技术门槛更高。播放只需要一个播放器,推流需要采集音视频、编码、封装、推送。
音频采集。 需要访问麦克风,采集 PCM 音频,编码为 AAC。
视频采集。 需要访问摄像头,采集 YUV 视频帧,编码为 H.264。
封装。 将音频和视频封装为 FLV 或 RTMP 格式。
推送。 通过 RTMP 协议推送到流媒体服务器。
在 UniApp 中,小程序和 App 的推流实现方式完全不同。
3.2 微信小程序推流
微信小程序提供了 live-pusher 组件用于推流。它封装了采集、编码和推送的全流程。
<!-- pages/live/push.vue --> <template> <view class="push-page"> <!-- #ifdef MP-WEIXIN --> <live-pusher ref="pusher" :url="pushUrl" mode="HD" :aspect="aspect" :beauty="beauty" :whiteness="whiteness" :muted="muted" :enable-camera="enableCamera" @statechange="onPushStateChange" @error="onPushError" /> <!-- #endif --> </view> </template> <script> export default { data() { return { pushUrl: '', aspect: '9:16', beauty: 5, whiteness: 5, muted: false, enableCamera: true, }; }, async onLoad(options) { // 创建直播间或获取已有直播间的推流地址 const res = await this.$api.live.createRoom({ title: options.title || '直播', }); this.pushUrl = res.data.push_url; }, methods: { onPushStateChange(e) { // 推送状态变化 const code = e.detail.code; if (code === 1002) { // 已开始推流,通知后端更新直播间状态 this.$api.live.start(this.roomId); } }, onPushError(e) { console.error('推流错误', e); }, startPush() { // #ifdef MP-WEIXIN this.$refs.pusher.start(); // #endif }, stopPush() { // #ifdef MP-WEIXIN this.$refs.pusher.stop(); // #endif }, }, }; </script>
小程序推流的工程约束是:需要开通小程序的直播权限。微信对 live-pusher 和 live-player 组件有类目限制,只有特定类目(如电商、教育、医疗)的小程序才能使用。这是多商户系统需要处理的问题——每个商户的小程序都需要单独申请权限。
3.3 App 端推流
App 端的推流需要集成原生推流 SDK。UniApp 没有内置推流组件,需要使用原生插件。
常见的推流 SDK 包括:腾讯云 MLVB SDK、阿里云推流 SDK、声网 Agora、ijkplayer(仅播放)。
// utils/live/appPusher.js /** * App 端推流封装 * 依赖原生插件:LivePusher */ class AppPusher { constructor() { this.pusher = null; } init(options) { // #ifdef APP-PLUS this.pusher = uni.requireNativePlugin('LivePusher'); this.pusher.init({ url: options.pushUrl, mode: options.mode || 'HD', aspect: options.aspect || '9:16', beauty: options.beauty || 5, whiteness: options.whiteness || 5, audioQuality: 'high', videoQuality: 'high', }, (result) => { if (result.code === 0) { console.log('推流初始化成功'); } else { console.error('推流初始化失败', result); } }); // #endif } start() { // #ifdef APP-PLUS if (this.pusher) { this.pusher.start(); } // #endif } stop() { // #ifdef APP-PLUS if (this.pusher) { this.pusher.stop(); } // #endif } switchCamera() { // #ifdef APP-PLUS if (this.pusher) { this.pusher.switchCamera(); } // #endif } destroy() { // #ifdef APP-PLUS if (this.pusher) { this.pusher.destroy(); this.pusher = null; } // #endif } } export const appPusher = new AppPusher();
3.4 H5 端不支持推流的原因
H5 端在浏览器中运行,无法直接访问设备的摄像头和麦克风进行 RTMP 推流。虽然浏览器提供了 getUserMedia API 访问摄像头和麦克风,但浏览器无法直接将音视频流推送到 RTMP 服务器。
浏览器的推流方案是 WebRTC,但 WebRTC 需要服务端支持,且与 RTMP 不兼容。如果系统需要支持 H5 推流,需要额外的 WebRTC 服务端和协议转换。
对于多商户商城系统,H5 端不做推流是合理的选择。商家用 App 或小程序推流,观众用任意终端观看,这已经覆盖了绝大多数使用场景。
四、直播与商城的业务耦合
4.1 直播间的商品管理
直播间与商品的关联是直播带货的核心。主播在直播过程中可以切换讲解的商品,观众在直播间看到当前讲解的商品并直接下单。
CREATE TABLE `live_product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `room_id` int(11) NOT NULL, `product_id` int(11) NOT NULL, `sort` tinyint(4) NOT NULL DEFAULT '0', `is_active` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否正在讲解', `active_at` datetime DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_room_product` (`room_id`, `product_id`), KEY `idx_room_sort` (`room_id`, `sort`), KEY `idx_active` (`room_id`, `is_active`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
is_active 字段标记当前正在讲解的商品。同一时刻,一个直播间只有一个商品是 is_active = 1。当主播切换商品时,系统更新这个字段,并通过 WebSocket 推送消息给所有观众,观众的界面自动滚动到当前讲解的商品。
4.2 直播订单的归因
直播订单需要归因到具体的直播间和主播。这个归因用于计算主播的佣金和商户的直播效果分析。
ALTER TABLE `order` ADD COLUMN `live_room_id` int(11) DEFAULT NULL COMMENT '直播订单归因的直播间ID' AFTER `merchant_id`; ALTER TABLE `order` ADD KEY `idx_live_room` (`live_room_id`);
当用户在直播间下单时,订单记录 live_room_id。这样商户和主播可以看到:本场直播带来了多少订单、多少 GMV。
4.3 实时互动:弹幕、点赞、下单通知
直播间的实时互动通过 WebSocket 实现。弹幕、点赞、下单通知都需要实时推送给所有观众。
<?php // live_server.php — 基于 Workerman 的直播 WebSocket 服务 use Workerman\Worker; use Workerman\Connection\TcpConnection; $ws = new Worker('websocket://0.0.0.0:8282'); $ws->count = 4; $rooms = []; $ws->onConnect = function (TcpConnection $connection) { $connection->roomId = null; $connection->userId = null; }; $ws->onMessage = function (TcpConnection $connection, $data) use (&$rooms) { $msg = json_decode($data, true); if (!$msg || !isset($msg['type'])) return; switch ($msg['type']) { case 'join': $roomId = (int)$msg['room_id']; $connection->roomId = $roomId; $connection->userId = (int)$msg['user_id']; if (!isset($rooms[$roomId])) { $rooms[$roomId] = []; } $rooms[$roomId][$connection->id] = $connection; broadcast($rooms[$roomId], [ 'type' => 'viewer_count', 'count' => count($rooms[$roomId]), ]); break; case 'danmaku': if ($connection->roomId) { broadcast($rooms[$connection->roomId] ?? [], [ 'type' => 'danmaku', 'user_id' => $connection->userId, 'content' => mb_substr($msg['content'], 0, 50), 'timestamp' => time(), ]); } break; case 'like': if ($connection->roomId) { broadcast($rooms[$connection->roomId] ?? [], [ 'type' => 'like', 'user_id' => $connection->userId, 'count' => $msg['count'] ?? 1, ]); } break; case 'product_active': // 主播切换讲解商品(仅主播可操作) if ($connection->roomId) { broadcast($rooms[$connection->roomId] ?? [], [ 'type' => 'product_active', 'product_id' => (int)$msg['product_id'], ]); } break; case 'order_notify': // 下单通知(由后端订单服务触发) if ($connection->roomId) { broadcast($rooms[$connection->roomId] ?? [], [ 'type' => 'order_notify', 'nickname' => $msg['nickname'], 'product_name' => $msg['product_name'], ]); } break; } }; $ws->onClose = function (TcpConnection $connection) use (&$rooms) { if ($connection->roomId && isset($rooms[$connection->roomId])) { unset($rooms[$connection->roomId][$connection->id]); if (empty($rooms[$connection->roomId])) { unset($rooms[$connection->roomId]); } else { broadcast($rooms[$connection->roomId], [ 'type' => 'viewer_count', 'count' => count($rooms[$connection->roomId]), ]); } } }; function broadcast(array $connections, array $message) { $json = json_encode($message); foreach ($connections as $conn) { $conn->send($json); } } Worker::runAll();
这个 WebSocket 服务的核心设计是房间隔离。每个直播间是一个独立的房间,消息只在房间内广播。这保证了不同商户的直播间不会互相干扰。
五、直播回放与存储
5.1 回放的生成
直播结束后,流媒体服务器可以自动将直播流录制为视频文件。SRS 支持通过配置开启录制,录制文件保存为 FLV 或 MP4。
# SRS 配置:开启录制 vhost __defaultVhost__ { dvr { enabled on; dvr_path ./objs/nginx/html/[app]/[stream].[timestamp].flv; dvr_plan session; # 按会话录制 } }
录制完成后,系统需要将回放地址写入直播间记录,并触发转码(将 FLV 转为 HLS 以便在 H5 端播放)。
<?php // app/common/service/LiveReplayService.php namespace app\common\service; use think\facade\Db; use think\facade\Queue; class LiveReplayService { /** * 直播结束后处理回放 */ public function handleReplay(int $roomId, string $recordPath): void { // 异步转码 Queue::push('app\job\TranscodeReplay', [ 'room_id' => $roomId, 'record_path' => $recordPath, ], 'video'); Db::name('live_room') ->where('id', $roomId) ->update([ 'status' => 2, 'ended_at' => date('Y-m-d H:i:s'), 'replay_record_path' => $recordPath, ]); } /** * 转码回放(队列消费者) */ public function transcode(array $data): void { $roomId = $data['room_id']; $source = $data['record_path']; // 转码为 HLS(多码率) $outputDir = "/var/www/replay/{$roomId}/"; if (!is_dir($outputDir)) { mkdir($outputDir, 0755, true); } $cmd = sprintf( 'ffmpeg -i %s -c:v libx264 -preset medium -b:v 1500k ' . '-c:a aac -b:a 128k -f hls -hls_time 10 ' . '-hls_list_size 0 -hls_segment_filename "%s/seg_%%03d.ts" ' . '%s/index.m3u8 2>&1', escapeshellarg($source), $outputDir, $outputDir ); exec($cmd, $output, $returnCode); if ($returnCode !== 0) { \think\facade\Log::error('回放转码失败', [ 'room_id' => $roomId, 'output' => implode("\n", $output), ]); return; } // 更新回放地址 $replayUrl = "/replay/{$roomId}/index.m3u8"; Db::name('live_room') ->where('id', $roomId) ->update(['replay_url' => $replayUrl]); } }
5.2 回放的播放
回放的播放比直播简单,因为它不需要低延迟。HLS 是回放的首选协议,因为它在所有终端上的兼容性最好。
// 回放播放地址选择 export function selectReplayUrl(room) { // 所有终端统一使用 HLS return room.replay_url; }
六、边界与张力
多商户商城直播系统在工程实践中,有几个需要清醒认识的边界。
第一,小程序的直播权限限制。 微信对 live-pusher 和 live-player 组件有类目限制。不是所有商户的小程序都能开通直播权限。多商户系统需要提供"直播功能降级"方案:没有直播权限的小程序只能观看 H5 端的直播(通过 web-view 嵌入),或者只能观看回放。
第二,推流的带宽成本。 推流消耗上行带宽,拉流消耗下行带宽。一场 1000 人观看的直播,按 720p 码率(1.5Mbps)计算,每分钟消耗约 11GB 下行流量。多商户平台的带宽成本会随直播场次和观众数快速累积。
第三,直播延迟与互动的矛盾。 HTTP-FLV 的延迟约 3-5 秒,HLS 的延迟约 10-30 秒。观众看到的画面和主播的实际动作有时间差。这导致互动(如"主播看这里")有时效性问题。如果要降低延迟,需要使用 WebRTC,但 WebRTC 的服务器成本和复杂度更高。
第四,多商户的内容审核。 直播是实时内容,审核难度远高于图文。系统需要提供实时审核能力(AI 识别违规画面 + 人工抽检),以及紧急关停机制。
第五,直播录制的存储成本。 每场直播的录制文件可能达到数 GB。多商户平台需要制定存储策略:保留多久、是否转码、是否清理。对象存储(如 S3、OSS)是比本地磁盘更合适的选择。
结语:全终端直播的工程本质是"协议适配 + 能力降级"
多商户商城直播系统的核心工程挑战,可以归结为一句话:在有限的终端能力下,提供最接近一致的直播体验。
播放侧,系统需要根据终端能力选择协议:小程序和 App 优先 HTTP-FLV(低延迟),H5 根据浏览器能力选择 FLV 或 HLS。播放器动态加载,避免首屏负担。
推流侧,小程序使用 live-pusher 组件,App 使用原生推流 SDK,H5 不支持推流。这是终端能力的客观限制,工程上的应对是"能力降级"而非"强行突破"。
业务侧,直播间、商品、订单、互动消息都需要与多商户架构正确耦合。每个商户的直播间是独立的,商品是隔离的,订单是归属明确的。
这套系统的价值,最终体现在一个朴素的判断上:一个商户用 App 开播,观众用小程序、H5 或 App 都能看到直播并下单,直播结束后回放可以随时观看。 这个"都能看到"就是全终端直播的工程底线。
#免责声明#
本站不存储任何实质资源,该帖为网盘用户发布的网盘链接介绍帖,本文内所有链接指向的云盘网盘资源,其版权归版权方所有!其实际管理权为帖子发布者所有,本站无法操作相关资源。如您认为本站任何介绍帖侵犯了您的合法版权,请发送邮件 linkshow2025@163.com 进行投诉,我们将在确认本文链接指向的资源存在侵权后,立即删除相关介绍帖子!
盘主最近主题
- 1《秘书工作手记》套装共3册 职场心法 公文写作 助你在办公室混得风生水起[pdf]
- 2清华大学:DeepSeek从入门到精通 & 职场赋能【高清版PDF】- 15.9MB
- 3《B站 - 职场生存图鉴:拿下令你心动的 offer》【2.90 GB】
- 4《职场高手的12堂工作汇报术(1) 》【5.00 GB】
- 5《职场新人必修课 职场新人路径 》【8.85 GB】
- 6《职场人性沟通术学会再学说话(1) 》【3.01 GB】
- 7《职场生存拿下令你心动的offer 》【3.02 GB】
- 8《黄先生的职场独家秘笈·求职篇 》 【10.72 GB】
- 9职场蔡老师人性沟通课【3.01 GB】
- 10《职场高手的12堂工作汇报术》【5.15 GB】
职场区
暂无评论