Files
as-daycard-openapi/HANDOVER.md
zhaogd 03e5f6fbf2 feat: 全局数据隔离 + 响应结构统一 + 交接文档
1. 新增SysOrgService业务数据隔离工具方法(applyBusinessOrgScope/getUserOrgMpath)
2. 10个Controller接入组织数据隔离(设备/订单/退款/统计/定价/支付配置/激活码/用户)
3. 统一响应结构:写操作从R.ok()改为R.dataSuccess(data)
4. 修复5处data.data双层嵌套
5. 修复SysUserController.info()不一致
6. 修复循环依赖(@Lazy)
7. 新增交接文档HANDOVER.md
2026-08-11 15:50:36 +08:00

286 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# as-daycard-openapi 项目交接文档
## 一、项目概述
天卡(24小时制视频权益卡)开放API平台,为酒店电视设备提供天卡购买、支付、激活、权益管理的完整能力。设备端通过本平台完成微信支付购买天卡,支付成功后异步调用橘视厅接口激活权益,设备获取 `thirdCardNo` 凭证后直连橘视厅SDK播放内容。
**技术栈**: Java 8 / Spring Boot 2.6.6 / MyBatis-Plus / MySQL / Redis / Shiro + JWT / Quartz / wechatpay-java SDK 0.2.14
**部署**: Docker (eclipse-temurin:17-jre-alpine),端口 8084,Spring Profile = prod
---
## 二、组织架构与数据隔离
### 四层组织体系
```
平台(Platform) > 渠道(Channel) > 代理商(Agent/SERVICE_PROVIDER) > 门店(Store)
```
- 每个组织节点有 `mpath` 物化路径(如 `1.2.3.`),用于快速判断层级关系
- 用户通过 `sys_user.org_id` 关联到某个组织节点
- `org_id = NULL` 为超级管理员,不限制数据范围
### 数据隔离机制
- **隔离工具**: `SysOrgService.applyBusinessOrgScope(wrapper, orgColumn, operatorId)` —— 向业务表 QueryWrapper 注入子查询,按 `sys_org.mpath` 前缀匹配过滤
- **权限校验**: `SysOrgService.isOrgInScope(orgId, operatorId)` —— 判断目标组织是否在操作人范围内
- **已接入隔离的模块**: 设备、订单、退款、统计、定价、支付配置、激活码、用户管理
- **平台级资源不隔离**: 天卡池(全局库存)、系统菜单
---
## 三、核心业务流程
### 3.1 设备初始化与鉴权
```
设备 App 启动
-> POST /api/jst/init (body: activationCode + mac/deviceSn/deviceUid)
-> 后端创建/更新设备记录,生成 sign
-> 返回 { sign, deviceId }
-> 后续所有请求在 header 携带 sign
```
- **激活码模式**: 设备通过管理后台生成的门店激活码初始化,绑定到门店组织
- **代销模式**: 第三方合作伙伴的设备免激活码,通过设备标识匹配已注册设备
- **sign 机制**: 后端 `ApiDeviceSignFilter` 验证 sign,解析出 deviceId 放入 request attribute。sign 有时效,可通过 `heartBeat` 续期或 `refreshSign` 刷新
### 3.2 天卡购买流程
```
1. 设备查询权益 -> POST /api/jst/checkBenefits -> UNBOUND(无权益)
2. 设备查询价格 -> POST /api/jst/price -> 返回价格
3. 设备创建订单 -> POST /api/jst/daycard/create
-> Redis 防重锁(按 deviceId)
-> 解析设备组织链路(设备 -> 门店 -> 渠道 -> 代理商)
-> 3层定价匹配(门店 > 渠道 > 平台默认)
-> 插入订单(状态 PENDING)
-> 调微信 Native 下单,获取 code_url
-> SKIP LOCKED 抢卡(UNUSED -> RESERVED)
-> 返回 { orderNo, codeUrl, amount, expireAt }
4. App 用 codeUrl 生成二维码,用户扫码支付
5. 用户取消支付 -> POST /api/jst/daycard/cancel/{orderNo}
-> 校验订单归属 -> 微信关单 -> 释放卡(RESERVED -> UNUSED)-> 订单 CANCELLED -> 清理 Redis 锁
6. 用户支付成功 -> 微信回调 -> 异步激活
```
### 3.3 支付回调与异步激活
```
微信支付回调 -> POST /openapi/payment/wechat/notify
-> 验签(从 header 提取 Wechatpay-Serial/Nonce/Signature/Timestamp,HttpServletRequest.getHeader() 大小写不敏感)
-> 解密获取交易信息
-> 幂等检查(订单已 PAID 则直接返回)
-> 乐观锁更新订单 PAID + 卡状态 RESERVED -> USED
-> 异步激活(@Async):
1. jushitingApiService.getToken(device, thirdCardNo) -> 获取 token + memberCode
2. jushitingApiService.activateCard(deviceId, cardCode, memberCode, token) -> 激活卡密
3. jushitingApiService.queryVipExpireTime(deviceId, thirdCardNo) -> 查询VIP到期时间
4. 更新 day_card_pool: expire_at + activate_status=SUCCESS
-> 清理 Redis 防重锁
```
### 3.4 橘视厅权益映射
```
关键标识 thirdCardNo:
- 卡密导入时自动生成(D + 24位随机字符)
- 存储在 day_card_pool.third_card_no
- 作为橘视厅的 uid 和 mac 参数(mac = thirdCardNo + "_bul_" + businessLine)
- 设备通过 checkBenefits 获取 thirdCardNo
- 设备用 thirdCardNo 直连橘视厅获取 token,激活卡密,获得 VIP 权益
设备与卡的关联:
day_card_pool.device_id -> device.id -> device.mac/device_sn/device_uid
解绑时清除 device_id,卡状态保持 USED(终态不可复用)
```
### 3.5 卡状态流转
```
UNUSED -> RESERVED -> USED(终态)
导入 下单抢卡 支付成功
取消/超时: RESERVED -> UNUSED(释放回池)
退款: USED -> UNUSED(清除 device_id/order_no/expire_at/activate_status)
解绑: USED 保持不变,仅清除 device_id
```
### 3.6 代销模式(合作伙伴)
```
采购: 平台天卡池 UNUSED -> RESERVED,写入 partner_card_pool(status=AVAILABLE)
消耗: POST /openapi/partner/card/consume
-> partner 池取1张 AVAILABLE 卡
-> day_card_pool RESERVED -> USED + device_id
-> partner_card_pool AVAILABLE -> CONSUMED
-> 异步激活(同上)
-> App 后续用同一设备标识调 init + checkBenefits
回退: partner_card_pool AVAILABLE -> DISABLED,平台卡 RESERVED -> UNUSED
```
### 3.7 分账(需微信商户号开通分账权限)
```
支付成功后触发 executeSharing(orderNo):
-> 匹配分润规则(全局/渠道/代理商/门店四级)
-> 计算各方分账金额(比例或固定,非平台方总比例 ≤ 30%)
-> 创建 profit_sharing_order + profit_sharing_detail
-> 调微信 createProfitSharing API(设置 unfreezeUnsplit=true 自动解冻剩余)
-> 调微信 finishProfitSharing 完结分账
注意: 当前分账标记(SettleInfo.profitSharing=true)已注释,需商户号开通分账权限后取消注释
```
---
## 四、接口体系
### 4.1 设备端接口(/api/jst/,sign 鉴权)
| 接口 | 说明 |
|------|------|
| POST /api/jst/init | 设备初始化,返回 sign |
| POST /api/jst/checkBenefits | 查询天卡权益,返回 thirdCardNo |
| POST /api/jst/price | 查询天卡价格 |
| POST /api/jst/daycard/create | 创建天卡订单 |
| GET /api/jst/daycard/status/{orderNo} | 查询支付状态 |
| POST /api/jst/daycard/cancel/{orderNo} | 取消订单 |
| POST /api/jst/heartBeat | 心跳续期 |
| POST /api/jst/refreshSign | 刷新 sign |
### 4.2 管理后台接口(/manage/,JWT 鉴权)
覆盖:系统管理(用户/角色/菜单/组织/门店)、设备管理、天卡池/订单/退款/激活码、定价策略、支付配置、分账规则/记录、统计概览、合作伙伴管理、媒资管理等,共约 100+ 个接口。
### 4.3 合作伙伴接口(/openapi/partner/,appKey/appSecret 签名鉴权)
| 接口 | 说明 |
|------|------|
| POST /openapi/partner/card/consume | 消耗天卡(代销) |
| GET /openapi/partner/card/stock | 查询库存 |
| GET /openapi/partner/media/* | 媒资接口(内容包/栏目树/搜索等) |
### 4.4 微信回调接口(/openapi/payment/,无鉴权)
| 接口 | 说明 |
|------|------|
| POST /openapi/payment/wechat/notify | 支付回调 |
| POST /openapi/payment/refund/notify | 退款回调 |
| GET /openapi/payment/query/{orderNo} | 查询支付状态 |
### 4.5 统一响应结构
```json
{ "code": 0, "msg": "", "data": {} }
```
- code=0 成功,code!=0 失败(500 业务错误,403 权限错误)
- data 始终存在,列表分页用 `data.page`,详情用 `data.{业务字段}`
---
## 五、定价体系
三层维度优先级匹配:门店配置价 > 渠道配置价 > 平台默认价
| 配置层级 | channel_org_id | store_org_id |
|---------|---------------|-------------|
| 平台默认 | NULL | NULL |
| 渠道级 | 具体值 | NULL |
| 门店级 | NULL | 具体值 |
---
## 六、定时任务
| 任务 | 说明 |
|------|------|
| DayCardOrderTimeoutTask | 取消超时未支付订单(15分钟),释放卡 |
| DayCardActivateRetryTask | 重试激活失败的天卡 |
| ProfitSharingQueryTask | 查询分账状态,更新分账单 |
---
## 七、橘视厅平台对接
### 平台级凭证(用于内容/UI接口,非设备级)
```
SP_TOKEN = "9006874392099a89506142c6df9e3314"
KEY = "alltvjs"
PORTAL_CODE = "PCODE-JC"
sign = MD5(sptoken + timeshift + portalcode + key).toUpperCase()
```
App 用此凭证直连橘视厅 `/IPTVPortalInterface/` 下的接口(topButton、gettagslist、getcontentlist、search 等),获取内容和 UI。**不经过我们后端**。
### 设备级权益凭证(thirdCardNo)
- 由后端在卡密导入时自动生成
- 设备通过 `checkBenefits` 获取
- 用于橘视厅免密登录(getToken)和卡密激活(activateCard)
- `getToken` 的 uid 和 mac 参数都基于 thirdCardNo 构造,设备实体不传给橘视厅
### 橘视厅 API 基地址
- 正式环境: `https://api.jetsenx.huashi.tv/`
- 测试环境: `https://external.jetsenx.huashi.tv/`
- 媒资接口: `https://servhub-ep03.future.jetsentv.com/`
---
## 八、微信支付配置
- 支持多商户号配置(DB `wechat_pay_config` 表,四级优先级:门店 > 代理商 > 渠道 > 全局)
- DB 配置优先,回退到 yml 环境变量
- SDK 配置实例按 商户号+证书序列号+密钥+私钥 缓存,配置变更自动重建
- 回调验签支持多商户号(遍历所有 ACTIVE 配置逐个尝试解析)
---
## 九、待办与注意事项
1. **分账功能**: 代码已实现但分账标记(`SettleInfo.profitSharing=true`)已注释,需微信商户号开通分账权限后取消注释(WechatPayServiceImpl 和 DayCardOrderServiceImpl.payCallback 中)
2. **退款SDK**: `WechatPayServiceImpl.refund()` 目前是骨架实现(TODO 接入 wechatpay-java RefundService)
3. **设备标识改造**: 当前卡池用 `device_id`(Long, 自增) 关联设备,计划改为 `device_uid`(String) 关联,已搁置
4. **天卡池不隔离**: 天卡池为平台级库存资源,不按组织隔离,所有管理员可见
5. **媒资接口分页不统一**: 3个媒资相关端点用 `data.list` + `data.totalCount` 平铺式分页,与其他接口的 `data.page` 不同
6. **支付接口路径**: 当前支付接口在 `/openapi/payment/` 下,计划迁移到 `/payment/` 与第三方接口区分(未实施)
7. **密码哈希**: Shiro `Sha256Hash(password, salt)` 哈希顺序为 `salt + password`(不是 password + salt)
---
## 十、数据库与部署
### 数据库
- MySQL 8.0,数据库名 `jstopenapi`
- 完整建表脚本: `db/full-schema.sql`
- 初始化数据: `db/init-sys-data.sql`(菜单 + 角色 + 超管用户 admin/admin)
- 逻辑删除: `delete_flag` 字段(UNDEL/DEL)
### Redis
- 用于:设备 sign 缓存、防重锁、橘视厅 token 缓存、激活锁
- Key 前缀: `daycard:device:`(订单防重)、`api:thirdparty:activate:`(激活相关)
### 部署
```bash
# 构建
mvn clean package -DskipTests
# Docker 部署
docker-compose up -d
# 关键环境变量(docker-compose.yml 中配置)
SPRING_PROFILES_ACTIVE=prod
DB_URL / DB_USERNAME / DB_PASSWORD
REDIS_HOST / REDIS_PORT / REDIS_PASSWORD
JWT_SECRET / API_SIGN_SECRET
WECHAT_PAY_* # 微信支付配置(或通过 DB wechat_pay_config 表配置)
```
### 超管账号
- 用户名: admin / 密码: admin
- 新增超管: wangrui / 密码: Admin@2026.