# 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 |