18 KiB
18 KiB
MediaApp 核心业务逻辑文档
一、系统架构
┌─────────────────────────────────────────────────────────────┐
│ MediaApp (Android) │
│ │
│ HomeActivity ──→ DetailActivity ──→ VideoPlayerActivity │
│ │ │ │ │
│ │ ├─ PaymentViewModel │ │
│ │ ├─ JetsenxSdkUtil │ │
│ │ └─ DayCardApiService │ │
└───────┼───────────────┼────────────────────┘ │
│ │ │
▼ ▼ ▼
┌──────────────────────────────┐ ┌───────────────────────────┐
│ as-daycard-openapi (后端) │ │ 橘视厅/捷成平台 │
│ 端口: 8084 │ │ api.future.jetsentv.com │
│ │ │ │
│ ├─ 天卡订单管理 │ │ ├─ UMS用户系统 │
│ ├─ 天卡池管理 │ │ ├─ 卡密激活 │
│ ├─ 微信/Mock支付 │ │ ├─ VIP订购关系 │
│ ├─ 异步卡密激活 │──│──→ ├─ 视频地址分发 │
│ ├─ 权益查询(thirdCardNo) │ │ └─ 媒资管理 │
│ └─ 媒资/播放权限 │ │ │
│ │ │ │
│ 数据库: MySQL (dev-0418) │ │ │
│ 缓存: Redis │ │ │
└──────────────────────────────┘ └───────────────────────────┘
二、完整播放流程
流程图
用户点击播放
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 1: 检查天卡播放权限 │
│ App → GET /openapi/media/play-permission/{mediaId} │
│ ?deviceId=1 │
│ 后端: 查 day_card_pool 表 │
│ status=USED AND expire_at > NOW() │
│ │
│ ┌─ 有权限 ──→ HasPermission ──→ 跳到 Step 3 │
│ └─ 无权限 ──→ 进入支付流程 │
└─────────────────────────────────────────────────────────┘
│ (无权限)
▼
┌─────────────────────────────────────────────────────────┐
│ Step 2: 天卡支付流程 │
│ │
│ 2a. 创建订单 │
│ App → POST /openapi/daycard/create │
│ 后端: Redis防重锁 → SELECT FOR UPDATE SKIP LOCKED │
│ 抢一张UNUSED卡 → 卡状态UNUSED→RESERVED │
│ → 创建订单(CREATED) │
│ │
│ 2b. 支付 │
│ App → POST /openapi/payment/mock-pay/{orderNo} │
│ 后端: 订单→PAID, 卡→USED, expire_at=24h兜底 │
│ → @Async asyncActivateCard() │
│ │
│ 2c. 轮询支付状态 │
│ App → GET /openapi/daycard/status/{orderNo} │
│ 直到返回 PAID │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 2d: 后端异步激活卡密 (3步HTTP调用橘视厅API) │
│ │
│ ① getToken(device, thirdCardNo) │
│ POST api.future.jetsentv.com/ums_api/api/v1/ │
│ third/login │
│ 参数: uid=thirdCardNo, mac, accesser, businessLine │
│ 签名: RSA(jushitingLoginRsaPublicKey) │
│ 返回: token + memberCode │
│ │
│ ② activateCard(deviceId, cardCode, memberCode, token) │
│ POST api.future.jetsentv.com/ccms_api/hswj/api/ │
│ card/activate │
│ 参数: activateCode=cardCode, memberCode │
│ 签名: MD5(activateCode&memberCode×tamp&key) │
│ 返回: status=true + expireTime │
│ ★ 此时橘视厅平台建立了 thirdCardNo→用户→订购关系 │
│ │
│ ③ queryVipExpireTime(thirdCardNo) [仅②未返回expireTime] │
│ POST api.future.jetsentv.com/ums_api/member/uid/ │
│ queryVipInfo │
│ 返回: vipExpireTime │
│ │
│ 激活完成: day_card_pool.activate_status=SUCCESS │
│ expire_at = 真实到期时间 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 3: 查询天卡权益获取 thirdCardNo │
│ App → GET /openapi/daycard/benefits/{deviceId} │
│ 后端: 查 day_card_pool │
│ status=USED AND expire_at > NOW() │
│ 返回: { thirdCardNo, hasBenefits, cardCode, expireAt } │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 4: SDK初始化 (用 thirdCardNo 作为用户身份) │
│ JetsenxSdkUtil.init(ctx, thirdCardNo, callback) │
│ │
│ SDK内部: GET api.future.jetsentv.com/ums_api/openapi/ │
│ init?uid=thirdCardNo&utype=2 │
│ 参数: agcode, accesser=JSTASJD, mac, tenant=huashitv │
│ secret, serviceComboCode │
│ 返回: memberCode (用于后续视频地址获取) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 5: 获取视频地址 │
│ JetsenxSdkUtil.getAllVideoUrl(parentContentType, │
│ programCode) │
│ │
│ SDK内部流程: │
│ 5a. getAllAssetPrices → 获取 productCode │
│ 5b. parentContentType=="8" → getFreeVideoUrls (免费) │
│ parentContentType!="8" → getVideoUrls (付费) │
│ 5c. getVideoUrls: │
│ GET api.future.jetsentv.com/bms_api/openapi/ │
│ orderAuth?memberCode&uid&productCode&programCode│
│ 有订购→返回VideoInfo[] (1080p/720p地址) │
│ 无订购→-110错误 │
│ 5d. -110时回退getFreeVideoUrls │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 6: 播放 │
│ 跳转 VideoPlayerActivity │
│ ExoPlayer 播放视频URL │
└─────────────────────────────────────────────────────────┘
三、防重复消费机制
三层防护确保不会浪费卡密
| 防护层 | 机制 | 位置 | 效果 |
|---|---|---|---|
| 第1层 | App端 checkPermission | PaymentViewModel Step1 | 24小时内重复点击不创建订单 |
| 第2层 | 后端 Redis防重锁 | createOrder() 第1步 | 同一设备订单超时时间内只能一个订单 |
| 第3层 | 定时任务释放超时订单 | DayCardOrderTimeoutTask | 订单超时未支付,卡密回收为UNUSED |
权益有效期
- 天卡支付成功后获得 24小时 播放权益
- 期间无限次播放不额外消耗卡密
- 权益过期后再次播放需要重新支付
四、关键配置项
4.1 SDK配置 (JetsenxSdkUtil.kt)
| 参数 | 值 | 说明 |
|---|---|---|
| AAR包 | JetsenxSdk-api-future-release-v1.23.aar |
生产域名 api.future.jetsentv.com |
| AGCODE | 1100101010200492358120169 |
渠道号 |
| TENANT | huashitv |
租户 |
| ACCESSER | JSTASJD |
合作方标识(生产环境) |
| SECRET | kMeWsBu0QsTXpnft |
秘钥(生产环境) |
| SERVICE_COMBO_CODE | 1100121010200494755850172 |
业务组合编码 |
4.2 后端橘视厅平台配置 (sys_config: JST_PLATFORM_CONFIG)
| 参数 | 值 |
|---|---|
| jushitingBaseUrl | https://api.future.jetsentv.com |
| vipQueryBaseUrl | https://api.future.jetsentv.com |
| jushitingTenant | huashitv |
| jushitingAccesser | JSTASJD |
| jushitingBusinessLine | 4100450011381755998420012 |
| jushitingServiceComboCode | 1100121010200494755850172 |
| jushitingLoginRsaPublicKey | (RSA公钥) |
| jushitingActivateSignKey | hhDEHY1qC4r |
| vipQuerySignKey | c25svtwbTyCE |
4.3 thirdCardNo 生成规则
| 项目 | 规则 | 示例 |
|---|---|---|
| as-daycard-openapi | D + 24位随机(0-9A-Za-z) |
D00BB8E623D9280D1ACABE803 |
| as-partner-backend | 24位随机(0-9A-Za-z) | A3f9Kp2m...(无前缀) |
D 前缀确保两个项目的 thirdCardNo 空间完全隔离,不会碰撞。
五、关键文件索引
MediaApp (Android端)
| 文件 | 作用 |
|---|---|
ui/DetailActivity.kt |
详情页,播放流程入口 |
ui/VideoPlayerActivity.kt |
ExoPlayer播放器 |
ui/HomeActivity.kt |
首页,媒资列表 |
sdk/JetsenxSdkUtil.kt |
SDK封装:初始化+获取视频地址 |
network/DayCardApiService.kt |
天卡后端API接口定义 |
network/DayCardRetrofitClient.kt |
Retrofit客户端 |
viewmodel/PaymentViewModel.kt |
天卡支付流程状态机 |
viewmodel/DetailViewModel.kt |
详情页数据 |
model/daycard/DayCardModels.kt |
天卡数据模型 |
config/DayCardConfig.kt |
天卡配置(设备ID/金额) |
config/ApiConfig.kt |
媒资API配置 |
as-daycard-openapi (后端)
| 文件 | 作用 |
|---|---|
daycard/controller/DayCardOrderController.java |
订单/权益接口 |
daycard/service/impl/DayCardOrderServiceImpl.java |
订单核心逻辑+异步激活 |
daycard/service/impl/DayCardPoolServiceImpl.java |
天卡池管理+thirdCardNo生成 |
daycard/task/DayCardOrderTimeoutTask.java |
超时订单清理 |
jst/service/impl/JushitingApiServiceImpl.java |
橘视厅API调用 |
media/service/impl/MediaServiceImpl.java |
播放权限检查 |
openapi/filter/PartnerSignFilter.java |
签名鉴权过滤器 |
openapi/sign/impl/PartnerSignServiceImpl.java |
HMAC-SHA256签名 |
六、项目环境信息
6.1 目录结构
/Users/zhaoguodong/Desktop/
├── jiecheng_business/ # 工作目录(本项目)
│ ├── MediaApp/ # Android客户端 (Kotlin)
│ ├── MediaApp核心业务逻辑文档.md # 本文档
│ └── README.md
├── as-daycard-openapi/ # 本项目后端 (Spring Boot, 端口8084)
│ └── src/main/resources/mapper/sys/SysConfigDao.xml # ★手动创建的MyBatis映射
└── as-partner-backend/ # 生产环境后端 (仅参考,端口8083,不在本项目中)
6.2 构建环境
| 项目 | 构建命令 | 说明 |
|---|---|---|
| MediaApp | JAVA_HOME=.../jbr-17.0.14 && cd MediaApp && ./gradlew assembleDebug |
APK输出: MediaApp/app/build/outputs/apk/debug/app-debug.apk |
| as-daycard-openapi | cd as-daycard-openapi && mvn spring-boot:run -Dspring-boot.run.profiles=dev |
端口8084 |
| JAVA_HOME | /Users/zhaoguodong/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home |
JDK 17 |
| 安装到设备 | adb install -r app-debug.apk |
6.3 后端数据库
| 项目 | 值 |
|---|---|
| MySQL | 127.0.0.1:3306 |
| 数据库 | dev-0418 |
| 用户/密码 | root / r2356510 |
| mysql路径 | /opt/homebrew/Cellar/mysql@8.0/8.0.42/bin/mysql |
| Redis | 127.0.0.1:6379 database 0 |
6.4 API基础地址
| 项目 | 地址 |
|---|---|
| 天卡后端 (as-daycard-openapi) | http://172.16.100.67:3002 (通过nginx代理到8084) |
| 橘视厅平台 | https://api.future.jetsentv.com |
| 媒资API | 橘视厅SDK内部调用,域名同上 |
6.5 测试设备
| 项目 | 值 |
|---|---|
| deviceId | 1 (数据库device表id=1) |
| MAC | CC:8C:DA:C6:E9:9B |
| 测试卡密 | cardCode=4009B1C3926DF480, thirdCardNo=D00BB8E623D9280D1ACABE803 |
| 卡密状态 | USED, activate_status=SUCCESS, expire_at=2026-07-14 18:23:58 |
| 合作伙伴 | partnerId=1, appKey=71df57e7e47a4a88 |
七、API签名鉴权机制
App与天卡后端的所有 /openapi/** 接口(除 /openapi/partner/** 和 /openapi/payment/notify)都需要签名鉴权:
签名格式: Base64URL(appKey:timestamp:nonce:HMAC-SHA256(appKey:timestamp:nonce, appSecret))
请求头:
| Header | 说明 |
|---|---|
X-App-Key |
appKey |
X-Timestamp |
毫秒时间戳 |
X-Nonce |
随机字符串 |
X-Sign |
签名值 |
校验规则: HMAC比对 + 时间戳5分钟有效期 + Redis nonce防重放
八、PaymentViewModel 状态机
Idle
↓ startPaymentFlow()
CheckingPermission ──→ HasPermission ──→ fetchVideoUrlAndPlay()
↓ (无权限)
CreatingOrder ──→ OrderCreated ──→ (用户确认支付)
↓
Paying ──→ Polling ──→ PaymentSuccess ──→ fetchVideoUrlAndPlay()
↓ (任意失败) ↓
Error Error
防重复跳转: DetailActivity.isFetchingVideoUrl 标志位,在 HasPermission/PaymentSuccess 时置true,跳转播放器后不会因 repeatOnLifecycle(STARTED) 重新触发。点击播放按钮时重置为false。
九、已知限制与待办
| 项目 | 说明 |
|---|---|
| 设备ID硬编码 | DayCardConfig.DEVICE_ID = 1L,多设备需要改为动态获取 |
| Mock支付 | 当前用 mock-pay 接口测试,生产需切换微信支付 |
| 单内容测试 | 仅验证了单个媒资播放,未测试列表翻页、搜索等 |
| 签名证书 | MediaApp未配置release签名,打release包时需要配置 |
| 后端签名校验 | App端API调用需实现HMAC签名(当前可能在Retrofit拦截器中) |
十、踩坑记录
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 详情页闪退 | onCreate中调用SDK初始化,native库加载异常 | 改为点击播放时按需初始化 |
| 16KB对齐警告 | exoplayer-ffmpeg-extension中的.so未对齐 | 移除未使用的FFmpeg扩展库 |
| NoClassDefFoundError: LogProducerConfig | 缺少火山引擎TLS日志库 | 添加tls-android-producer依赖 |
| 支付对话框闪退 | HasPermission时关闭对话框后SDK回调无法显示 | 保持对话框打开直到视频地址获取完成 |
| SDK返回-110未订购 | uid="test_user"无效 | 从benefits接口获取真实thirdCardNo |
| 后端激活失败: SysConfigDao.queryByKey not found | 缺少MyBatis XML映射 | 创建SysConfigDao.xml |
| SDK返回604 | SDK用external域名(测试),后端用api域名(生产) | 换用api-future AAR包 |
| memberCode不一致 | 两个域名指向不同服务器,用户数据不互通 | 统一使用api.future.jetsentv.com |