App Store Connect API 密钥是什么,为什么推荐用它登录?
'''# App Store Connect API 密钥是什么,为什么推荐用它登录?
App Store Connect API 密钥是 Apple 提供的一种无密码登录凭据,通过 Issuer ID、Key ID 和一把私钥(.p8 文件)生成短期 JWT 令牌来认证访问 App Store Connect,不需要输入 Apple ID 密码,也不会触发双重验证,且长期有效——适合团队协作、多设备使用或接入自动化流程。
和账号密码登录相比的三个优势
| 对比项 | 账号密码登录 | API 密钥登录 |
|---|---|---|
| 是否需要密码 | 需要 | 不需要 |
| 是否触发双重验证 | 会(首次) | 不会 |
| 是否长期有效、可多端共用 | 依赖登录会话 | 是,生成一次可在任意设备导入使用 |
怎么获取
在「开心上架(AppUploader)」的「账号概览」页面:
- 用账号密码方式先登录一次,进入「App Store Connect API 密钥」区域。
- 点击「生成密钥」,填写密钥名称,提交后会生成一把 ADMIN 权限、可访问全部 App 的密钥。
- 私钥(
.p8)只能获取这一次,务必立即复制或下载为.json密钥文件保存好。 - 之后可以直接拿这份
.json文件,在登录页选择「添加账号 → API 密钥 → 密钥文件」导入,在任意设备上登录同一个 App Store Connect 账号。
密钥列表随时支持「导出密钥文件」(前提是本机保存过该密钥的私钥)或「吊销」——吊销后由它派生的所有账号会立即无法认证,且不可恢复,请谨慎操作。
什么场景必须用 API 密钥
「开心上架」中部分功能明确要求使用 App Store Connect API 密钥或开发者门户账号登录,其他登录方式暂不支持,例如「应用」模块中的分类/年龄分级管理、内购项目管理。如果你需要用到这些能力,建议直接用 API 密钥方式登录。'''
'''# App Store Connect API 密钥是什么,为什么推荐用它登录?
App Store Connect API 密钥是 Apple 提供的一种无密码登录凭据,通过 Issuer ID、Key ID 和一把私钥(.p8 文件)生成短期 JWT 令牌来认证访问 App Store Connect,不需要输入 Apple ID 密码,也不会触发双重验证,且长期有效——适合团队协作、多设备使用或接入自动化流程。
和账号密码登录相比的三个优势
| 对比项 | 账号密码登录 | API 密钥登录 |
|---|---|---|
| 是否需要密码 | 需要 | 不需要 |
| 是否触发双重验证 | 会(首次) | 不会 |
| 是否长期有效、可多端共用 | 依赖登录会话 | 是,生成一次可在任意设备导入使用 |
怎么获取
在「开心上架(AppUploader)」的「账号概览」页面:
- 用账号密码方式先登录一次,进入「App Store Connect API 密钥」区域。
- 点击「生成密钥」,填写密钥名称,提交后会生成一把 ADMIN 权限、可访问全部 App 的密钥。
- 私钥(
.p8)只能获取这一次,务必立即复制或下载为.json密钥文件保存好。 - 之后可以直接拿这份
.json文件,在登录页选择「添加账号 → API 密钥 → 密钥文件」导入,在任意设备上登录同一个 App Store Connect 账号。
密钥列表随时支持「导出密钥文件」(前提是本机保存过该密钥的私钥)或「吊销」——吊销后由它派生的所有账号会立即无法认证,且不可恢复,请谨慎操作。
什么场景必须用 API 密钥
「开心上架」中部分功能明确要求使用 App Store Connect API 密钥或开发者门户账号登录,其他登录方式暂不支持,例如「应用」模块中的分类/年龄分级管理、内购项目管理。如果你需要用到这些能力,建议直接用 API 密钥方式登录。'''
收起阅读 »在线教育知识付费直播源码免费下载|UniApp 多端可用
仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open
凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。
凸知移动端 是一款基于 uni-app(Vue 2)+ TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。
系统演示:
| 端 | 演示地址 | 账号 | 密码 |
|---|---|---|---|
| 后台 | https://tuzhi.mutouweb.com/admin.php | admin | 123456 |
| 移动端(用户) | https://tuzhi.mutouweb.com | 13222222222 | 123456 |
| PC 端(用户) | https://tuzhi.mutouweb.com/pc/#/ | 13222222222 | 123456 |
一、功能特性
- 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
- 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
- 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
- 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
- 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
- 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
- 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
- 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
- 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)
界面预览:
二、系统架构
凸知是一套完整的知识付费系统,由四个部分组成:
| 部分 | 技术栈 | 说明 | 源码开放 |
|---|---|---|---|
| 后端服务 | ThinkPHP 5.0 + FastAdmin + MySQL | 接口服务,承载支付、直播、分销等全部业务逻辑 | 购买授权 |
| 移动端(本仓库) | uni-app(Vue 2)+ TDesign | H5 / 微信小程序学员端(不支持 App 打包) | 开源 |
| 管理后台 | Vue 3 + Arco Design | 管理员 / 讲师运营后台 | 购买授权 |
| PC 学员端 | Vue 3 + Arco Design | 电脑端学员学习页面 | 购买授权 |
凸知终端技术结构:
整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。
本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格。
三、技术栈
| 分类 | 技术 | 说明 |
|---|---|---|
| 跨端框架 | uni-app(Vue 2.x) | vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包) |
| 开发语言 | JavaScript(Vue 2 语法)+ Sass | 页面按 xxx.vue + js.vue + css.css 三文件拆分,样式用 CSS 规则 |
| UI 组件库 | TDesign(@tdesign/uniapp) |
全局主题样式,小程序端内置构建适配补丁(scripts/) |
uView(本地 components/uview-ui) |
列表 / 弹窗 / 表单等通用组件 | |
自研 components/tz/ |
音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45+ 业务组件 | |
| qiun-data-charts | 图表可视化 | |
| 状态管理 | Vuex 3 | 全局登录态 / 用户信息等 |
| 国际化 | vue-i18n 8 | 内置 zh-CN / US 多语言 |
| 网络请求 | flyio + 自封装 $api |
$api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转 |
| 音视频播放 | 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js | 直播与点播,支持倍速 / 弹幕 / 水印等组件 |
| 微信能力 | jweixin-module | 微信登录 / 分享 JS-SDK 封装 |
| 构建工具 | @vue/cli 5 + webpack | 由 @dcloudio/vue-cli-plugin-uni 驱动多端编译 |
| 测试 | Jest | 多端单元测试(npm run test:*) |
四、快速开始
环境要求:Node.js >= 14(建议 16/18)、npm
# ① 安装依赖
npm install
# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)
npm run dev:h5
浏览器访问 http://localhost:8080 即可看到完整页面效果。
微信小程序编译:
# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)
npm run dev:mp-weixin
# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)
npm run build:mp-weixin
用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。
对接真实后端:
- 修改
src/siteinfo.js中的siteroot为你的后端地址; - 在
src/common/request/api_list.js中核对接口地址与{code, msg, data}协议; - 微信小程序构建需在
src/manifest.json中替换为自己的 appid。
五、构建
| 目标 | 命令 |
|---|---|
| H5 | npm run build:h5 |
| 微信小程序 | npm run build:mp-weixin |
小程序构建内置 TDesign 适配补丁(
scripts/),构建成功后产物在dist/build/下。⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。
六、Mock 数据
- H5 开发模式(
NODE_ENV=development)默认关闭,全部走真实后端接口; - 开启方式:将
src/static/config.js的enableMock改为true(显式设置优先级最高),或通过uni.setStorageSync('enableMock', true)/ 环境变量MOCK_ENABLE切换; - 覆盖 200+ 接口,fixture 位于
src/mock/data/,按module/action路由镜像; - 未覆盖的接口自动透传真实网络并输出提示;
- Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。
七、目录结构
src/
├── pages/ 页面(index 主包 + public/app/user/order/course 五个分包,约 100+ 页面)
│ ├── index/ 首页(主包唯一页面,tabBar 入口)
│ ├── launch/ 启动页(加载与参数透传跳转,未在 pages.json 注册)
│ ├── public/ 公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等
│ ├── app/ 业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等
│ ├── user/ 用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等
│ ├── order/ 订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等
│ ├── course/ 课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等
│ └── template/ 页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)
├── components/ 组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)
├── common/ 公共层
│ ├── request/ 接口封装($api 点分路由、统一鉴权/错误处理)
│ ├── utils/ 工具(路由映射、支付、直播、OSS 签名等)
│ ├── order/ 订单状态处理
│ └── wechat/ 微信登录/分享 SDK 封装
├── mock/ Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)
├── static/ 静态资源
├── pages.json 页面路由与分包配置
└── manifest.json 应用配置(appid 等)
八、开发约定
- 页面采用
xxx.vue + js.vue + css.css三文件拆分(<script src="./js.vue">+@import "./css.css"),逻辑与样式分离; - 接口调用统一走
this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理; - 业务组件优先复用
components/tz/自研组件库; - 提交信息遵循 Conventional Commits(
feat:/fix:/chore:)。
九、相关链接
| 名称 | 链接 |
|---|---|
| 使用文档 | https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh |
| 功能列表 | https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe |
| 授权价格 | https://www.tuzhi.ltd/index/pricing/version.html |
| 功能演示 | https://www.tuzhi.ltd/index/demo/index.html |
十、软件安全
安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。
十一、使用许可
- 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE;
- 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);
仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open
凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。
凸知移动端 是一款基于 uni-app(Vue 2)+ TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。
系统演示:
| 端 | 演示地址 | 账号 | 密码 |
|---|---|---|---|
| 后台 | https://tuzhi.mutouweb.com/admin.php | admin | 123456 |
| 移动端(用户) | https://tuzhi.mutouweb.com | 13222222222 | 123456 |
| PC 端(用户) | https://tuzhi.mutouweb.com/pc/#/ | 13222222222 | 123456 |
一、功能特性
- 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
- 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
- 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
- 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
- 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
- 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
- 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
- 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
- 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)
界面预览:
二、系统架构
凸知是一套完整的知识付费系统,由四个部分组成:
| 部分 | 技术栈 | 说明 | 源码开放 |
|---|---|---|---|
| 后端服务 | ThinkPHP 5.0 + FastAdmin + MySQL | 接口服务,承载支付、直播、分销等全部业务逻辑 | 购买授权 |
| 移动端(本仓库) | uni-app(Vue 2)+ TDesign | H5 / 微信小程序学员端(不支持 App 打包) | 开源 |
| 管理后台 | Vue 3 + Arco Design | 管理员 / 讲师运营后台 | 购买授权 |
| PC 学员端 | Vue 3 + Arco Design | 电脑端学员学习页面 | 购买授权 |
凸知终端技术结构:
整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。
本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格。
三、技术栈
| 分类 | 技术 | 说明 |
|---|---|---|
| 跨端框架 | uni-app(Vue 2.x) | vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包) |
| 开发语言 | JavaScript(Vue 2 语法)+ Sass | 页面按 xxx.vue + js.vue + css.css 三文件拆分,样式用 CSS 规则 |
| UI 组件库 | TDesign(@tdesign/uniapp) |
全局主题样式,小程序端内置构建适配补丁(scripts/) |
uView(本地 components/uview-ui) |
列表 / 弹窗 / 表单等通用组件 | |
自研 components/tz/ |
音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45+ 业务组件 | |
| qiun-data-charts | 图表可视化 | |
| 状态管理 | Vuex 3 | 全局登录态 / 用户信息等 |
| 国际化 | vue-i18n 8 | 内置 zh-CN / US 多语言 |
| 网络请求 | flyio + 自封装 $api |
$api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转 |
| 音视频播放 | 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js | 直播与点播,支持倍速 / 弹幕 / 水印等组件 |
| 微信能力 | jweixin-module | 微信登录 / 分享 JS-SDK 封装 |
| 构建工具 | @vue/cli 5 + webpack | 由 @dcloudio/vue-cli-plugin-uni 驱动多端编译 |
| 测试 | Jest | 多端单元测试(npm run test:*) |
四、快速开始
环境要求:Node.js >= 14(建议 16/18)、npm
# ① 安装依赖
npm install
# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)
npm run dev:h5
浏览器访问 http://localhost:8080 即可看到完整页面效果。
微信小程序编译:
# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)
npm run dev:mp-weixin
# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)
npm run build:mp-weixin
用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。
对接真实后端:
- 修改
src/siteinfo.js中的siteroot为你的后端地址; - 在
src/common/request/api_list.js中核对接口地址与{code, msg, data}协议; - 微信小程序构建需在
src/manifest.json中替换为自己的 appid。
五、构建
| 目标 | 命令 |
|---|---|
| H5 | npm run build:h5 |
| 微信小程序 | npm run build:mp-weixin |
小程序构建内置 TDesign 适配补丁(
scripts/),构建成功后产物在dist/build/下。⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。
六、Mock 数据
- H5 开发模式(
NODE_ENV=development)默认关闭,全部走真实后端接口; - 开启方式:将
src/static/config.js的enableMock改为true(显式设置优先级最高),或通过uni.setStorageSync('enableMock', true)/ 环境变量MOCK_ENABLE切换; - 覆盖 200+ 接口,fixture 位于
src/mock/data/,按module/action路由镜像; - 未覆盖的接口自动透传真实网络并输出提示;
- Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。
七、目录结构
src/
├── pages/ 页面(index 主包 + public/app/user/order/course 五个分包,约 100+ 页面)
│ ├── index/ 首页(主包唯一页面,tabBar 入口)
│ ├── launch/ 启动页(加载与参数透传跳转,未在 pages.json 注册)
│ ├── public/ 公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等
│ ├── app/ 业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等
│ ├── user/ 用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等
│ ├── order/ 订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等
│ ├── course/ 课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等
│ └── template/ 页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)
├── components/ 组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)
├── common/ 公共层
│ ├── request/ 接口封装($api 点分路由、统一鉴权/错误处理)
│ ├── utils/ 工具(路由映射、支付、直播、OSS 签名等)
│ ├── order/ 订单状态处理
│ └── wechat/ 微信登录/分享 SDK 封装
├── mock/ Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)
├── static/ 静态资源
├── pages.json 页面路由与分包配置
└── manifest.json 应用配置(appid 等)
八、开发约定
- 页面采用
xxx.vue + js.vue + css.css三文件拆分(<script src="./js.vue">+@import "./css.css"),逻辑与样式分离; - 接口调用统一走
this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理; - 业务组件优先复用
components/tz/自研组件库; - 提交信息遵循 Conventional Commits(
feat:/fix:/chore:)。
九、相关链接
| 名称 | 链接 |
|---|---|
| 使用文档 | https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh |
| 功能列表 | https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe |
| 授权价格 | https://www.tuzhi.ltd/index/pricing/version.html |
| 功能演示 | https://www.tuzhi.ltd/index/demo/index.html |
十、软件安全
安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。
十一、使用许可
- 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE;
- 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);
组件截图(ComponentSnapshot)返回错误码100001,可能原因为截图尺寸过大,文档说明其与具体硬件限制有关,如何查看具体限制是多少
硬件限制因平台而异,可以通过如下命令进行查看:
hdc shell hidumper -s 10 -a 'vktextureLimit'
常见值为“width: 8192 height: 8192”,表示最大绘制纹理尺寸的长宽都需要在8192像素以内。比较待截图组件的尺寸,以便确认截图失败是否为该原因导致,如果是,请调整所截图组件的大小,或实现为滚动截图后自行拼接。实现请参考截取长内容(滚动截图)和长截图。
如需实现离屏组件的长截图,可参考以下实现:
// src/main/ets/utils/Utils.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class Utils {
static sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Calculate the valid screenshot area
static async getSnapshotArea(context: UIContext, pixelMap: PixelMap, scrollYOffsets: number[], listWidth: number,
listHeight: number): Promise<image.PositionArea> {
let stride = pixelMap.getBytesNumberPerRow();
let bytesNumber = pixelMap.getPixelBytesNumber();
let buffer: ArrayBuffer = new ArrayBuffer(bytesNumber);
let len = scrollYOffsets.length;
if (scrollYOffsets.length >= 2) {
let realScrollHeight = scrollYOffsets[len-1] - scrollYOffsets[len-2];
if (listHeight - realScrollHeight > 0) {
let cropRegion: image.Region = {
x: 0,
y: context.vp2px(listHeight - realScrollHeight) || 0,
size: {
height: context.vp2px(realScrollHeight) || 0,
width: context.vp2px(listWidth) || 0
}
};
await pixelMap.crop(cropRegion);
}
}
let area: image.PositionArea = {
pixels: buffer,
offset: 0,
stride: stride,
region: {
size: {
width: 0,
height: 0
},
x: 0,
y: 0
}
}
try {
let imgInfo = pixelMap.getImageInfoSync();
area.region.size.width = imgInfo.size.width;
area.region.size.height = imgInfo.size.height;
pixelMap.readPixelsSync(area);
} catch (err) {
let error = err as BusinessError;
console.error(`getSnapshotArea err, code:${error.code}, message: ${error.message}`);
}
return area;
}
// Graphic splicing
static async mergeImage(context: UIContext, areaArray: image.PositionArea[], lastOffsetY: number, listWidth: number,
listHeight: number): Promise<PixelMap> {
let opts: image.InitializationOptions = {
editable: true,
pixelFormat: 4,
size: {
width: context.vp2px(listWidth) || 0,
height: context.vp2px(lastOffsetY + listHeight) || 0
}
};
let longPixelMap = image.createPixelMapSync(opts);
let imgPosition: number = 0;
for (let i = 0; i < areaArray.length; i++) {
let readArea = areaArray[i];
let area: image.PositionArea = {
pixels: readArea.pixels,
offset: 0,
stride: readArea.stride,
region: {
size: {
width: readArea.region.size.width,
height: readArea.region.size.height
},
x: 0,
y: imgPosition
}
}
imgPosition += readArea.region.size.height;
try {
longPixelMap.writePixelsSync(area);
} catch (err) {
let error = err as BusinessError;
console.error(`writePixelsSync err, code:${error.code}, message: ${error.message}`);
}
}
return longPixelMap;
}
}
// src/main/ets/pages/Index.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Utils } from '../utils/Utils';
export class MyDataSource {
private _data: string[] = [];
private _listeners: DataChangeListener[] = [];
pushData(data: string): void {
this._data.push(data);
this._listeners.forEach(listener => {
listener.onDataAdd(this._data.length - 1);
})
}
getAllData(): string[] {
return this._data;
}
totalCount(): number {
return this._data.length;
}
getData(index: number): string {
return this._data[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this._listeners.push(listener);
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this._listeners.indexOf(listener);
if (index != -1) {
this._listeners.splice(index, 1);
}
}
}
@Entry
@Component
struct SnapshotExample {
private scroller: Scroller = new Scroller();
private listComponentWidth: number = 0;
private listComponentHeight: number = 0;
@State mergedImage: PixelMap | undefined = undefined;
private areaArray: image.PositionArea[] = [];
private scrollYOffsets: number[] = [];
private data: MyDataSource = new MyDataSource();
private listId: string = 'LIST_ID';
aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.data.pushData(Hello ${i});
}
}
async onceSnapshot() {
await this.beforeSnapshot();
await this.snapAndMerge();
this.afterGeneratorImage();
}
async snapAndMerge() {
try {
// Record the current scrolling position
this.scrollYOffsets.push(this.scroller.currentOffset().yOffset);
// Take a screenshot of the current display part of the component
const pixelMap = await this.getUIContext().getComponentSnapshot().get(this.listId);
// Calculate the valid screenshot area
let area: image.PositionArea =
await Utils.getSnapshotArea(this.getUIContext(), pixelMap, this.scrollYOffsets, this.listComponentWidth,
this.listComponentHeight);
this.areaArray.push(area);
// Determine whether to scroll to the bottom
if (!this.scroller.isAtEnd()) {
// Not to the bottom: Scroll down by one screen height
this.scroller.scrollTo({
xOffset: 0,
yOffset: (this.scroller.currentOffset().yOffset + this.listComponentHeight),
animation: {
duration: 200
}
});
await Utils.sleep(200);
await this.snapAndMerge();
} else {
this.mergedImage =
await Utils.mergeImage(this.getUIContext(), this.areaArray, this.scrollYOffsets[this.scrollYOffsets.length-1],
this.listComponentWidth, this.listComponentHeight);
}
} catch (err) {
let error = err as BusinessError;
console.error(snapAndMerge err, code:${error.code}, message: ${error.message});
}
}
async beforeSnapshot() {
try {
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: {
duration: 200
}
});
await Utils.sleep(200);
} catch (err) {
let error = err as BusinessError;
console.error(beforeSnapshot err, code:${error.code}, message: ${error.message});
}
}
afterGeneratorImage() {
this.scrollYOffsets.length = 0;
this.areaArray.length = 0;
}
build() {
Column({ space: 12 }) {
Button('Click to get the snapshot')
.onClick(() => {
this.onceSnapshot();
})
Stack() {
// Screenshot component
List({ space: 12, scroller: this.scroller }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item)
.fontSize(50)
.height(50)
}
}
}, (item: number) => item.toString())
}
.scrollBar(BarState.Off)
.cachedCount(3)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.background_secondary'))
.id(this.listId)
.onAreaChange((oldValue, newValue) => {
this.listComponentWidth = newValue.width as number;
this.listComponentHeight = newValue.height as number;
})
// Set the Z-sequence to -1 to ensure that this component is invisible
.zIndex(-1)
// Use a mask to cover the screenshot area
Column()
.width('100%').height('100%').backgroundColor(Color.White)
// Long screenshot
Scroll() {
Image(this.mergedImage)
}
}
.width('100%')
.layoutWeight(1)
}
}
}
https://sites.google.com/view/4qpzqf30/home
https://sites.google.com/view/1ggrec61/home
https://sites.google.com/view/7dqpdy74/home
https://sites.google.com/view/5bcwfb15/home
https://sites.google.com/view/3ccqme12/home
https://sites.google.com/view/9fauxq30/home
https://sites.google.com/view/0cskdu29/home
https://sites.google.com/view/7ndhuv19/home
https://sites.google.com/view/2qfdbs75/home
https://sites.google.com/view/7tbfwn54/home
https://sites.google.com/view/8utwqh99/home
https://sites.google.com/view/8tmfsn03/home
https://sites.google.com/view/7hyniw30/home
https://sites.google.com/view/5vcrza32/home
https://sites.google.com/view/9bicoh33/home
https://sites.google.com/view/8mykyr89/home
https://sites.google.com/view/9dkwex15/home
https://sites.google.com/view/3jdidd23/home
https://sites.google.com/view/4jnbko13/home
https://sites.google.com/view/3hiqwy37/home
https://sites.google.com/view/9sdhji16/home
https://sites.google.com/view/9vjvuk08/home
https://sites.google.com/view/4vtgzg10/home
https://sites.google.com/view/9simwj81/home
https://sites.google.com/view/2fhkhe54/home
https://sites.google.com/view/3ifqol91/home
https://sites.google.com/view/5oaygy48/home
https://sites.google.com/view/9uzuby80/home
https://sites.google.com/view/4mfobg52/home
https://sites.google.com/view/7lmnbx80/home
https://sites.google.com/view/7zieer80/home
https://sites.google.com/view/2wsnxl35/home
https://sites.google.com/view/9upiju57/home
https://sites.google.com/view/0cjgci51/home
https://sites.google.com/view/6bbfhi31/home
https://sites.google.com/view/5kfsgr23/home
https://sites.google.com/view/2yahzy66/home
https://sites.google.com/view/3bjdfe66/home
https://sites.google.com/view/8zysdc97/home
https://sites.google.com/view/4whfwi14/home
https://sites.google.com/view/4oxzdw14/home
https://sites.google.com/view/3xtxxz40/home
https://sites.google.com/view/3zfzrk48/home
https://sites.google.com/view/1vuurx26/home
https://sites.google.com/view/5fqasw51/home
https://sites.google.com/view/4auyde58/home
https://sites.google.com/view/3gztws07/home
https://sites.google.com/view/1unpun49/home
https://sites.google.com/view/3jclpx68/home
https://sites.google.com/view/8vfpdb61/home
https://sites.google.com/view/1jskfv65/home
https://sites.google.com/view/8khlye43/home
https://sites.google.com/view/0jlpsf46/home
https://sites.google.com/view/4rrmuk82/home
https://sites.google.com/view/8pyjpk92/home
https://sites.google.com/view/6skokh74/home
https://sites.google.com/view/6xhhhj64/home
https://sites.google.com/view/3zdgnv39/home
https://sites.google.com/view/3krsoe29/home
https://sites.google.com/view/4qccqa54/home
https://sites.google.com/view/6bmcza90/home
https://sites.google.com/view/9myclp29/home
https://sites.google.com/view/8chakc99/home
https://sites.google.com/view/7naelf98/home
https://sites.google.com/view/9ovzhg81/home
https://sites.google.com/view/3yvfkx78/home
https://sites.google.com/view/5cdvsn19/home
https://sites.google.com/view/1huyew52/home
https://sites.google.com/view/8lgdoh08/home
https://sites.google.com/view/2nqdur07/home
https://sites.google.com/view/5scjnw53/home
https://sites.google.com/view/9xvbba99/home
https://sites.google.com/view/3nqkgc40/home
https://sites.google.com/view/8gvcqc86/home
https://sites.google.com/view/9qpetf93/home
https://sites.google.com/view/7gkigb30/home
https://sites.google.com/view/9ghmex19/home
https://sites.google.com/view/1wcrpp04/home
https://sites.google.com/view/8udpqi35/home
https://sites.google.com/view/3canhn37/home
https://sites.google.com/view/7emxvq67/home
https://sites.google.com/view/1glhsm08/home
https://sites.google.com/view/1rbpie37/home
https://sites.google.com/view/6lknzd58/home
https://sites.google.com/view/0wjrrz98/home
https://sites.google.com/view/1kozsf11/home
https://sites.google.com/view/6hjwzy13/home
https://sites.google.com/view/9hhvpx75/home
https://sites.google.com/view/2wnusu60/home
https://sites.google.com/view/5dwcsn95/home
https://sites.google.com/view/8nwpaq06/home
硬件限制因平台而异,可以通过如下命令进行查看:
hdc shell hidumper -s 10 -a 'vktextureLimit'
常见值为“width: 8192 height: 8192”,表示最大绘制纹理尺寸的长宽都需要在8192像素以内。比较待截图组件的尺寸,以便确认截图失败是否为该原因导致,如果是,请调整所截图组件的大小,或实现为滚动截图后自行拼接。实现请参考截取长内容(滚动截图)和长截图。
如需实现离屏组件的长截图,可参考以下实现:
// src/main/ets/utils/Utils.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class Utils {
static sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Calculate the valid screenshot area
static async getSnapshotArea(context: UIContext, pixelMap: PixelMap, scrollYOffsets: number[], listWidth: number,
listHeight: number): Promise<image.PositionArea> {
let stride = pixelMap.getBytesNumberPerRow();
let bytesNumber = pixelMap.getPixelBytesNumber();
let buffer: ArrayBuffer = new ArrayBuffer(bytesNumber);
let len = scrollYOffsets.length;
if (scrollYOffsets.length >= 2) {
let realScrollHeight = scrollYOffsets[len-1] - scrollYOffsets[len-2];
if (listHeight - realScrollHeight > 0) {
let cropRegion: image.Region = {
x: 0,
y: context.vp2px(listHeight - realScrollHeight) || 0,
size: {
height: context.vp2px(realScrollHeight) || 0,
width: context.vp2px(listWidth) || 0
}
};
await pixelMap.crop(cropRegion);
}
}
let area: image.PositionArea = {
pixels: buffer,
offset: 0,
stride: stride,
region: {
size: {
width: 0,
height: 0
},
x: 0,
y: 0
}
}
try {
let imgInfo = pixelMap.getImageInfoSync();
area.region.size.width = imgInfo.size.width;
area.region.size.height = imgInfo.size.height;
pixelMap.readPixelsSync(area);
} catch (err) {
let error = err as BusinessError;
console.error(`getSnapshotArea err, code:${error.code}, message: ${error.message}`);
}
return area;
}
// Graphic splicing
static async mergeImage(context: UIContext, areaArray: image.PositionArea[], lastOffsetY: number, listWidth: number,
listHeight: number): Promise<PixelMap> {
let opts: image.InitializationOptions = {
editable: true,
pixelFormat: 4,
size: {
width: context.vp2px(listWidth) || 0,
height: context.vp2px(lastOffsetY + listHeight) || 0
}
};
let longPixelMap = image.createPixelMapSync(opts);
let imgPosition: number = 0;
for (let i = 0; i < areaArray.length; i++) {
let readArea = areaArray[i];
let area: image.PositionArea = {
pixels: readArea.pixels,
offset: 0,
stride: readArea.stride,
region: {
size: {
width: readArea.region.size.width,
height: readArea.region.size.height
},
x: 0,
y: imgPosition
}
}
imgPosition += readArea.region.size.height;
try {
longPixelMap.writePixelsSync(area);
} catch (err) {
let error = err as BusinessError;
console.error(`writePixelsSync err, code:${error.code}, message: ${error.message}`);
}
}
return longPixelMap;
}
}
// src/main/ets/pages/Index.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Utils } from '../utils/Utils';
export class MyDataSource {
private _data: string[] = [];
private _listeners: DataChangeListener[] = [];
pushData(data: string): void {
this._data.push(data);
this._listeners.forEach(listener => {
listener.onDataAdd(this._data.length - 1);
})
}
getAllData(): string[] {
return this._data;
}
totalCount(): number {
return this._data.length;
}
getData(index: number): string {
return this._data[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this._listeners.push(listener);
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this._listeners.indexOf(listener);
if (index != -1) {
this._listeners.splice(index, 1);
}
}
}
@Entry
@Component
struct SnapshotExample {
private scroller: Scroller = new Scroller();
private listComponentWidth: number = 0;
private listComponentHeight: number = 0;
@State mergedImage: PixelMap | undefined = undefined;
private areaArray: image.PositionArea[] = [];
private scrollYOffsets: number[] = [];
private data: MyDataSource = new MyDataSource();
private listId: string = 'LIST_ID';
aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.data.pushData(Hello ${i});
}
}
async onceSnapshot() {
await this.beforeSnapshot();
await this.snapAndMerge();
this.afterGeneratorImage();
}
async snapAndMerge() {
try {
// Record the current scrolling position
this.scrollYOffsets.push(this.scroller.currentOffset().yOffset);
// Take a screenshot of the current display part of the component
const pixelMap = await this.getUIContext().getComponentSnapshot().get(this.listId);
// Calculate the valid screenshot area
let area: image.PositionArea =
await Utils.getSnapshotArea(this.getUIContext(), pixelMap, this.scrollYOffsets, this.listComponentWidth,
this.listComponentHeight);
this.areaArray.push(area);
// Determine whether to scroll to the bottom
if (!this.scroller.isAtEnd()) {
// Not to the bottom: Scroll down by one screen height
this.scroller.scrollTo({
xOffset: 0,
yOffset: (this.scroller.currentOffset().yOffset + this.listComponentHeight),
animation: {
duration: 200
}
});
await Utils.sleep(200);
await this.snapAndMerge();
} else {
this.mergedImage =
await Utils.mergeImage(this.getUIContext(), this.areaArray, this.scrollYOffsets[this.scrollYOffsets.length-1],
this.listComponentWidth, this.listComponentHeight);
}
} catch (err) {
let error = err as BusinessError;
console.error(snapAndMerge err, code:${error.code}, message: ${error.message});
}
}
async beforeSnapshot() {
try {
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: {
duration: 200
}
});
await Utils.sleep(200);
} catch (err) {
let error = err as BusinessError;
console.error(beforeSnapshot err, code:${error.code}, message: ${error.message});
}
}
afterGeneratorImage() {
this.scrollYOffsets.length = 0;
this.areaArray.length = 0;
}
build() {
Column({ space: 12 }) {
Button('Click to get the snapshot')
.onClick(() => {
this.onceSnapshot();
})
Stack() {
// Screenshot component
List({ space: 12, scroller: this.scroller }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item)
.fontSize(50)
.height(50)
}
}
}, (item: number) => item.toString())
}
.scrollBar(BarState.Off)
.cachedCount(3)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.background_secondary'))
.id(this.listId)
.onAreaChange((oldValue, newValue) => {
this.listComponentWidth = newValue.width as number;
this.listComponentHeight = newValue.height as number;
})
// Set the Z-sequence to -1 to ensure that this component is invisible
.zIndex(-1)
// Use a mask to cover the screenshot area
Column()
.width('100%').height('100%').backgroundColor(Color.White)
// Long screenshot
Scroll() {
Image(this.mergedImage)
}
}
.width('100%')
.layoutWeight(1)
}
}
}
https://sites.google.com/view/4qpzqf30/home
https://sites.google.com/view/1ggrec61/home
https://sites.google.com/view/7dqpdy74/home
https://sites.google.com/view/5bcwfb15/home
https://sites.google.com/view/3ccqme12/home
https://sites.google.com/view/9fauxq30/home
https://sites.google.com/view/0cskdu29/home
https://sites.google.com/view/7ndhuv19/home
https://sites.google.com/view/2qfdbs75/home
https://sites.google.com/view/7tbfwn54/home
https://sites.google.com/view/8utwqh99/home
https://sites.google.com/view/8tmfsn03/home
https://sites.google.com/view/7hyniw30/home
https://sites.google.com/view/5vcrza32/home
https://sites.google.com/view/9bicoh33/home
https://sites.google.com/view/8mykyr89/home
https://sites.google.com/view/9dkwex15/home
https://sites.google.com/view/3jdidd23/home
https://sites.google.com/view/4jnbko13/home
https://sites.google.com/view/3hiqwy37/home
https://sites.google.com/view/9sdhji16/home
https://sites.google.com/view/9vjvuk08/home
https://sites.google.com/view/4vtgzg10/home
https://sites.google.com/view/9simwj81/home
https://sites.google.com/view/2fhkhe54/home
https://sites.google.com/view/3ifqol91/home
https://sites.google.com/view/5oaygy48/home
https://sites.google.com/view/9uzuby80/home
https://sites.google.com/view/4mfobg52/home
https://sites.google.com/view/7lmnbx80/home
https://sites.google.com/view/7zieer80/home
https://sites.google.com/view/2wsnxl35/home
https://sites.google.com/view/9upiju57/home
https://sites.google.com/view/0cjgci51/home
https://sites.google.com/view/6bbfhi31/home
https://sites.google.com/view/5kfsgr23/home
https://sites.google.com/view/2yahzy66/home
https://sites.google.com/view/3bjdfe66/home
https://sites.google.com/view/8zysdc97/home
https://sites.google.com/view/4whfwi14/home
https://sites.google.com/view/4oxzdw14/home
https://sites.google.com/view/3xtxxz40/home
https://sites.google.com/view/3zfzrk48/home
https://sites.google.com/view/1vuurx26/home
https://sites.google.com/view/5fqasw51/home
https://sites.google.com/view/4auyde58/home
https://sites.google.com/view/3gztws07/home
https://sites.google.com/view/1unpun49/home
https://sites.google.com/view/3jclpx68/home
https://sites.google.com/view/8vfpdb61/home
https://sites.google.com/view/1jskfv65/home
https://sites.google.com/view/8khlye43/home
https://sites.google.com/view/0jlpsf46/home
https://sites.google.com/view/4rrmuk82/home
https://sites.google.com/view/8pyjpk92/home
https://sites.google.com/view/6skokh74/home
https://sites.google.com/view/6xhhhj64/home
https://sites.google.com/view/3zdgnv39/home
https://sites.google.com/view/3krsoe29/home
https://sites.google.com/view/4qccqa54/home
https://sites.google.com/view/6bmcza90/home
https://sites.google.com/view/9myclp29/home
https://sites.google.com/view/8chakc99/home
https://sites.google.com/view/7naelf98/home
https://sites.google.com/view/9ovzhg81/home
https://sites.google.com/view/3yvfkx78/home
https://sites.google.com/view/5cdvsn19/home
https://sites.google.com/view/1huyew52/home
https://sites.google.com/view/8lgdoh08/home
https://sites.google.com/view/2nqdur07/home
https://sites.google.com/view/5scjnw53/home
https://sites.google.com/view/9xvbba99/home
https://sites.google.com/view/3nqkgc40/home
https://sites.google.com/view/8gvcqc86/home
https://sites.google.com/view/9qpetf93/home
https://sites.google.com/view/7gkigb30/home
https://sites.google.com/view/9ghmex19/home
https://sites.google.com/view/1wcrpp04/home
https://sites.google.com/view/8udpqi35/home
https://sites.google.com/view/3canhn37/home
https://sites.google.com/view/7emxvq67/home
https://sites.google.com/view/1glhsm08/home
https://sites.google.com/view/1rbpie37/home
https://sites.google.com/view/6lknzd58/home
https://sites.google.com/view/0wjrrz98/home
https://sites.google.com/view/1kozsf11/home
https://sites.google.com/view/6hjwzy13/home
https://sites.google.com/view/9hhvpx75/home
https://sites.google.com/view/2wnusu60/home
https://sites.google.com/view/5dwcsn95/home
https://sites.google.com/view/8nwpaq06/home
App SSL Pinning 抓包怎么办?三种方法拿到明文
'''之前处理 App 的 SSL Pinning 一直走 Frida hook:挂进进程,把证书校验函数改掉,代理抓包就能解密。直到有次在 iOS 上碰到带反调试的目标,hook 脚本全废,越狱环境还差点被检测出来。那次之后我把 Frida 环境撤了——为排查一次接口问题养一台越狱设备,成本实在不划算。那阵子把 app ssl pinning 抓包的各种路数重新捋了一遍,发现真正靠谱的路线有三条,都不碰校验函数。这篇按路线拆开,每条讲清楚怎么操作、什么时候用。
pinning 挡的是中间人这一条路
代理抓包的本质是中间人:工具用自己的根证书动态签发目标域名的证书,客户端信任根证书,握手继续,明文在中间被看到。SSL Pinning 把这个信任链写死了——App 在代码里固定服务端证书的指纹或公钥,校验时不看"证书链是否可信",只看"是不是我写死的那个"。代理签发的证书对不上,握手被拒。pinning 的写法有两种:固定证书本身,或固定证书里的公钥,后者更常见——证书可以轮换,公钥换了才需要发版更新。无论哪种写法,校验逻辑都发生在 TLS 握手阶段,这也是后面所有方案能绕开它的根本原因。所以 pinning 堵死的是"中间人换证书"这一种玩法,绕过它的思路就是避开这条路径,下面三条路都是这个逻辑。
路线一:代理抓包加一键去 Pin
想继续走代理这条熟悉的路线,先处理证书绑定。TraceEagle 的代理抓包里有一键解除目标证书绑定:选中目标 App,把它的证书校验处理掉,再按常规代理流程抓,HTTPS 请求就能正常解密,流量列表里直接看明文。
操作流程:开代理会话 → 目标指向要抓的 App → 解除证书绑定 → 让 App 产生请求 → 列表里出现已解密的请求,请求头、body 都能读。iOS 上装根证书走描述文件,Android 端扫码安装,都是一次性的;会话支持多开,代理和网卡直抓可以同时跑,各自独立页签互不干扰。去 Pin 之后不想碰的域名还能加进放行名单原样透传,只解密目标范围。适合想保留代理调试习惯的场景,规则改写、断点这些能力也都在同一条链路上。
路线二:应用层抓包直接取明文
不做中间人,也就没有"证书对不对得上"的问题。应用层抓包从程序内部数据收发的必经点取数,拿到的本来就是明文,不依赖任何中间人证书。pinning 校验发生在握手环节,应用层取数在握手之后,校验已经过去,管不到这一层。
操作上选应用层抓包,指定目标程序,开抓后让 App 产生请求,流量列表里直接是明文。不想放过启动初期的鉴权流量,勾上"抓启动那一刻",让工具先关掉 App 再拉起,握手和 token 交换都能覆盖。iOS 端免越狱,Android 端免装证书,同一套界面看解密结果。遇到证书绑定加自研加密叠一起的顽固目标,这条是代理和网卡之外仅剩的可行路。
App SSL Pinning 抓包的关键,是换一条不经过证书校验的路,而不是跟校验函数硬碰。
路线三:网卡直抓加密钥源
想看最原始的流量,走网卡直抓:逐包拿全量数据,DNS、QUIC、ICMP、任意 TCP/UDP 都不漏,粒度到包。HTTPS 流量默认是密文,把会话密钥源加进去(add_key_source),密钥自动匹配,密文变明文,原始包和解密后的内容并排看。
网卡直抓不依赖代理设置,App 不认系统代理也没关系;配合进程归属,混杂流量里能看出是谁发的,不用靠 IP 猜。适合排查非 HTTP 协议、确认包级行为、或者目标 App 流量根本不走代理的场景。抓包粒度跟 Wireshark 一个层级,但省掉手动导 SSLKEYLOGFILE 那套配置,密钥源加一次,之后的会话自动匹配。
三条路怎么选
代理去 Pin 适合习惯代理调试、想保留改写断点能力的场景;应用层取明文适合 pinning 加自研加密的顽固目标,iOS/Android 都免环境;网卡直抓加密钥源适合底层分析和非 HTTP 流量。组合使用也常见:常规流量走代理去 Pin,顽固目标切应用层,底层怀疑走网卡,同一台机器上互不干扰。对比 Frida hook 那套:要越狱或 root、脚本跟着 App 版本迭代、还可能被反调试发现,三条路都不碰校验函数,没有这些附加条件。选型的判断标准就一条:目标 App 的防护在哪一层,就把抓取点放在校验之外的那一层。各自适用场景不同,按目标类型选,不用一招打天下。
> 提醒一句:pinning 抓包用于自家 App 的调试和合规测试;绕过他人 App 的安全机制不在讨论范围内。
顺带一提,抓到明文只是开始,同一条流量还能接着做断点改包:请求飞在半路拦下来,改完再放行或丢弃,验证服务端对异常参数的响应;规则改写可以做 Mock 和转发;压测右键单条,恒定 QPS 或并发,百分位到 p99.9。TraceEagle 这些能力都在一个界面里,排查链路不用换工具。'''
'''之前处理 App 的 SSL Pinning 一直走 Frida hook:挂进进程,把证书校验函数改掉,代理抓包就能解密。直到有次在 iOS 上碰到带反调试的目标,hook 脚本全废,越狱环境还差点被检测出来。那次之后我把 Frida 环境撤了——为排查一次接口问题养一台越狱设备,成本实在不划算。那阵子把 app ssl pinning 抓包的各种路数重新捋了一遍,发现真正靠谱的路线有三条,都不碰校验函数。这篇按路线拆开,每条讲清楚怎么操作、什么时候用。
pinning 挡的是中间人这一条路
代理抓包的本质是中间人:工具用自己的根证书动态签发目标域名的证书,客户端信任根证书,握手继续,明文在中间被看到。SSL Pinning 把这个信任链写死了——App 在代码里固定服务端证书的指纹或公钥,校验时不看"证书链是否可信",只看"是不是我写死的那个"。代理签发的证书对不上,握手被拒。pinning 的写法有两种:固定证书本身,或固定证书里的公钥,后者更常见——证书可以轮换,公钥换了才需要发版更新。无论哪种写法,校验逻辑都发生在 TLS 握手阶段,这也是后面所有方案能绕开它的根本原因。所以 pinning 堵死的是"中间人换证书"这一种玩法,绕过它的思路就是避开这条路径,下面三条路都是这个逻辑。
路线一:代理抓包加一键去 Pin
想继续走代理这条熟悉的路线,先处理证书绑定。TraceEagle 的代理抓包里有一键解除目标证书绑定:选中目标 App,把它的证书校验处理掉,再按常规代理流程抓,HTTPS 请求就能正常解密,流量列表里直接看明文。
操作流程:开代理会话 → 目标指向要抓的 App → 解除证书绑定 → 让 App 产生请求 → 列表里出现已解密的请求,请求头、body 都能读。iOS 上装根证书走描述文件,Android 端扫码安装,都是一次性的;会话支持多开,代理和网卡直抓可以同时跑,各自独立页签互不干扰。去 Pin 之后不想碰的域名还能加进放行名单原样透传,只解密目标范围。适合想保留代理调试习惯的场景,规则改写、断点这些能力也都在同一条链路上。
路线二:应用层抓包直接取明文
不做中间人,也就没有"证书对不对得上"的问题。应用层抓包从程序内部数据收发的必经点取数,拿到的本来就是明文,不依赖任何中间人证书。pinning 校验发生在握手环节,应用层取数在握手之后,校验已经过去,管不到这一层。
操作上选应用层抓包,指定目标程序,开抓后让 App 产生请求,流量列表里直接是明文。不想放过启动初期的鉴权流量,勾上"抓启动那一刻",让工具先关掉 App 再拉起,握手和 token 交换都能覆盖。iOS 端免越狱,Android 端免装证书,同一套界面看解密结果。遇到证书绑定加自研加密叠一起的顽固目标,这条是代理和网卡之外仅剩的可行路。
App SSL Pinning 抓包的关键,是换一条不经过证书校验的路,而不是跟校验函数硬碰。
路线三:网卡直抓加密钥源
想看最原始的流量,走网卡直抓:逐包拿全量数据,DNS、QUIC、ICMP、任意 TCP/UDP 都不漏,粒度到包。HTTPS 流量默认是密文,把会话密钥源加进去(add_key_source),密钥自动匹配,密文变明文,原始包和解密后的内容并排看。
网卡直抓不依赖代理设置,App 不认系统代理也没关系;配合进程归属,混杂流量里能看出是谁发的,不用靠 IP 猜。适合排查非 HTTP 协议、确认包级行为、或者目标 App 流量根本不走代理的场景。抓包粒度跟 Wireshark 一个层级,但省掉手动导 SSLKEYLOGFILE 那套配置,密钥源加一次,之后的会话自动匹配。
三条路怎么选
代理去 Pin 适合习惯代理调试、想保留改写断点能力的场景;应用层取明文适合 pinning 加自研加密的顽固目标,iOS/Android 都免环境;网卡直抓加密钥源适合底层分析和非 HTTP 流量。组合使用也常见:常规流量走代理去 Pin,顽固目标切应用层,底层怀疑走网卡,同一台机器上互不干扰。对比 Frida hook 那套:要越狱或 root、脚本跟着 App 版本迭代、还可能被反调试发现,三条路都不碰校验函数,没有这些附加条件。选型的判断标准就一条:目标 App 的防护在哪一层,就把抓取点放在校验之外的那一层。各自适用场景不同,按目标类型选,不用一招打天下。
> 提醒一句:pinning 抓包用于自家 App 的调试和合规测试;绕过他人 App 的安全机制不在讨论范围内。
顺带一提,抓到明文只是开始,同一条流量还能接着做断点改包:请求飞在半路拦下来,改完再放行或丢弃,验证服务端对异常参数的响应;规则改写可以做 Mock 和转发;压测右键单条,恒定 QPS 或并发,百分位到 p99.9。TraceEagle 这些能力都在一个界面里,排查链路不用换工具。'''
收起阅读 »uniapp 实现 的小红书 1v1 还原
- 笔记创建 / 编辑 / 发布、点赞评论回复、付费笔记
私域直播(视频 / 语音、货架带货、公屏、体验倒计时、结束复盘)
音视频聊天室(麦位、心跳、体验时长)
IM:私信 / 群聊、直播与聊天室 邀请卡片
音视频通话(TRTC)
地图选点(笔记标记地点 / 腾讯地图)
市集电商(商品 / 订单 / 优惠券 / 秒杀拼团等)
后台:笔记 / 频道 / 话题、会员、商城、站点品牌、系统消息
系统消息、点点助手
iOS 卡顿排查:三个问题快速定位性能瓶颈
卡顿的本质
iOS 屏幕每 16.67ms 刷新一次(120Hz 设备是 8.33ms)。一帧内的工作没做完,就会错过刷新,表现为掉帧。所以排查卡顿,核心就一个问题:这一帧的时间被谁用掉了?
拆开来看只有三个环节:主线程、渲染、GPU。
第一问:主线程忙不忙?
主线程负责布局、图片解码、文本排版。这些事任何一件超过 16ms,直接掉帧。
常见的大头:
- Auto Layout:复杂视图层级下约束求解很慢,列表滚动时反复触发更严重。
- 图片解码:不预先解码,第一次显示时主线程解码,大图直接破帧。
- 圆角和阴影:会触发离屏渲染,CPU 要先画一份额外图像再合成。
- 富文本排版:多行文本、动态字体,排版计算不便宜。
怎么解决?原则只有一个:能提前做的别拖到主线程当下做。图片异步解码、圆角预先画好、布局结果缓存、文本排版预计算。
第二问:渲染准备有没有坑?
主线程干完活,Core Animation 要把更新打包交给 GPU。这一步的坑比较隐蔽:
- 视图层级太深:几十层 view 叠在一起,每层都有固定成本。
- 半透明太多:透明视图需要混合计算,大面积叠加会明显变慢。
- 重写了 drawRect:一旦重写,就绕开 GPU 走 CPU 绘制,能不用就不用。
排查方法很简单,Simulator 里打开 Color Blended Layers,绿色正常,红色就是要优化的混合区域,让红色尽量消失。
第三问:GPU 扛得住吗?
GPU 的负载看两个数:设备利用率和 Tile 利用率。
设备利用率接近 100%,GPU 已经满了;Tile 利用率超过 85%,多半是纹理问题——纹理太大、没压缩、没用 Mipmap。
优化方向:
- 大图用 ASTC 压缩格式
- 不透明 view 设置
isOpaque = true,减少无效绘制 - 减少离屏渲染的触发
别只看 FPS
FPS 是一秒内的平均值,会掩盖问题。一秒内前 30 帧 8ms、后 30 帧 25ms,平均还是 55 FPS,但用户会感到明显卡顿。
真正该关注的是帧间隔的稳定性。单帧延迟的尖峰,才是卡顿的真实来源。高刷设备上尤其明显。
排查顺序
- 看帧延迟曲线,确认卡顿是周期性还是无规律
- 看主线程占用,判断是不是 CPU 瓶颈
- 看 GPU 利用率,判断是不是 GPU 瓶颈
- 针对具体环节优化,改完对比帧延迟分布,而不是只看 FPS
核心就一句话:每一帧都赶上车,比平均速度好看更重要。
卡顿的本质
iOS 屏幕每 16.67ms 刷新一次(120Hz 设备是 8.33ms)。一帧内的工作没做完,就会错过刷新,表现为掉帧。所以排查卡顿,核心就一个问题:这一帧的时间被谁用掉了?
拆开来看只有三个环节:主线程、渲染、GPU。
第一问:主线程忙不忙?
主线程负责布局、图片解码、文本排版。这些事任何一件超过 16ms,直接掉帧。
常见的大头:
- Auto Layout:复杂视图层级下约束求解很慢,列表滚动时反复触发更严重。
- 图片解码:不预先解码,第一次显示时主线程解码,大图直接破帧。
- 圆角和阴影:会触发离屏渲染,CPU 要先画一份额外图像再合成。
- 富文本排版:多行文本、动态字体,排版计算不便宜。
怎么解决?原则只有一个:能提前做的别拖到主线程当下做。图片异步解码、圆角预先画好、布局结果缓存、文本排版预计算。
第二问:渲染准备有没有坑?
主线程干完活,Core Animation 要把更新打包交给 GPU。这一步的坑比较隐蔽:
- 视图层级太深:几十层 view 叠在一起,每层都有固定成本。
- 半透明太多:透明视图需要混合计算,大面积叠加会明显变慢。
- 重写了 drawRect:一旦重写,就绕开 GPU 走 CPU 绘制,能不用就不用。
排查方法很简单,Simulator 里打开 Color Blended Layers,绿色正常,红色就是要优化的混合区域,让红色尽量消失。
第三问:GPU 扛得住吗?
GPU 的负载看两个数:设备利用率和 Tile 利用率。
设备利用率接近 100%,GPU 已经满了;Tile 利用率超过 85%,多半是纹理问题——纹理太大、没压缩、没用 Mipmap。
优化方向:
- 大图用 ASTC 压缩格式
- 不透明 view 设置
isOpaque = true,减少无效绘制 - 减少离屏渲染的触发
别只看 FPS
FPS 是一秒内的平均值,会掩盖问题。一秒内前 30 帧 8ms、后 30 帧 25ms,平均还是 55 FPS,但用户会感到明显卡顿。
真正该关注的是帧间隔的稳定性。单帧延迟的尖峰,才是卡顿的真实来源。高刷设备上尤其明显。
排查顺序
- 看帧延迟曲线,确认卡顿是周期性还是无规律
- 看主线程占用,判断是不是 CPU 瓶颈
- 看 GPU 利用率,判断是不是 GPU 瓶颈
- 针对具体环节优化,改完对比帧延迟分布,而不是只看 FPS
核心就一句话:每一帧都赶上车,比平均速度好看更重要。
收起阅读 »全栈接单|uni‑app / 小程序 / App 开发|二开、Bug 修复、后台开发
承接各类软件外包开发
✅技术范围
uni‑app /uni‑app‑x
微信小程序|抖音小程序
App 端(安卓 /iOS)
前端页面|后端接口|管理后台
✅可做业务
全新定制开发
项目二次开发
Bug 修复调试
功能迭代、页面改版
接口对接、后台搭建
有需求直接发需求文档 / 原型,快速评估报价工期,高效交付。
联系:15622289461 (微信同号)
承接各类软件外包开发
✅技术范围
uni‑app /uni‑app‑x
微信小程序|抖音小程序
App 端(安卓 /iOS)
前端页面|后端接口|管理后台
✅可做业务
全新定制开发
项目二次开发
Bug 修复调试
功能迭代、页面改版
接口对接、后台搭建
有需求直接发需求文档 / 原型,快速评估报价工期,高效交付。
联系:15622289461 (微信同号)
stripe 支付不支持 ACH银行卡支付
uniapp 提供的stripe 支付无法实现,ACH 美国银行支付 , https://uniapp.dcloud.net.cn/tutorial/app-payment-stripe.html#%E5%BC%80%E9%80%9A
uniapp 提供的stripe 支付无法实现,ACH 美国银行支付 , https://uniapp.dcloud.net.cn/tutorial/app-payment-stripe.html#%E5%BC%80%E9%80%9A
河北、雄安周边有没有靠谱的开发个人或者开发团队
雄安新区周边有没有靠谱的开发个人或者团队,可以长期合作。项目稳定。主要是做公司内部需求开发、报表开发等基本不涉及流程类功能开发。可以联系19933338642
雄安新区周边有没有靠谱的开发个人或者团队,可以长期合作。项目稳定。主要是做公司内部需求开发、报表开发等基本不涉及流程类功能开发。可以联系19933338642
知识付费直播系统-Uniapp源码
仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open
凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。
凸知移动端 是一款基于 uni-app(Vue 2)+ TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。
系统演示:
| 端 | 演示地址 | 账号 | 密码 |
|---|---|---|---|
| 后台 | https://tuzhi.mutouweb.com/admin.php | admin | 123456 |
| 移动端(用户) | https://tuzhi.mutouweb.com | 13222222222 | 123456 |
| PC 端(用户) | https://tuzhi.mutouweb.com/pc/#/ | 13222222222 | 123456 |
一、功能特性
- 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
- 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
- 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
- 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
- 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
- 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
- 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
- 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
- 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)
界面预览:
二、系统架构
凸知是一套完整的知识付费系统,由四个部分组成:
| 部分 | 技术栈 | 说明 | 源码开放 |
|---|---|---|---|
| 后端服务 | ThinkPHP 5.0 + FastAdmin + MySQL | 接口服务,承载支付、直播、分销等全部业务逻辑 | 购买授权 |
| 移动端(本仓库) | uni-app(Vue 2)+ TDesign | H5 / 微信小程序学员端(不支持 App 打包) | 开源 |
| 管理后台 | Vue 3 + Arco Design | 管理员 / 讲师运营后台 | 购买授权 |
| PC 学员端 | Vue 3 + Arco Design | 电脑端学员学习页面 | 购买授权 |
凸知终端技术结构:
整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。
本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格。
三、技术栈
| 分类 | 技术 | 说明 |
|---|---|---|
| 跨端框架 | uni-app(Vue 2.x) | vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包) |
| 开发语言 | JavaScript(Vue 2 语法)+ Sass | 页面按 xxx.vue + js.vue + css.css 三文件拆分,样式用 CSS 规则 |
| UI 组件库 | TDesign(@tdesign/uniapp) |
全局主题样式,小程序端内置构建适配补丁(scripts/) |
uView(本地 components/uview-ui) |
列表 / 弹窗 / 表单等通用组件 | |
自研 components/tz/ |
音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45+ 业务组件 | |
| qiun-data-charts | 图表可视化 | |
| 状态管理 | Vuex 3 | 全局登录态 / 用户信息等 |
| 国际化 | vue-i18n 8 | 内置 zh-CN / US 多语言 |
| 网络请求 | flyio + 自封装 $api |
$api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转 |
| 音视频播放 | 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js | 直播与点播,支持倍速 / 弹幕 / 水印等组件 |
| 微信能力 | jweixin-module | 微信登录 / 分享 JS-SDK 封装 |
| 构建工具 | @vue/cli 5 + webpack | 由 @dcloudio/vue-cli-plugin-uni 驱动多端编译 |
| 测试 | Jest | 多端单元测试(npm run test:*) |
四、快速开始
环境要求:Node.js >= 14(建议 16/18)、npm
# ① 安装依赖
npm install
# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)
npm run dev:h5
浏览器访问 http://localhost:8080 即可看到完整页面效果。
微信小程序编译:
# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)
npm run dev:mp-weixin
# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)
npm run build:mp-weixin
用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。
对接真实后端:
- 修改
src/siteinfo.js中的siteroot为你的后端地址; - 在
src/common/request/api_list.js中核对接口地址与{code, msg, data}协议; - 微信小程序构建需在
src/manifest.json中替换为自己的 appid。
五、构建
| 目标 | 命令 |
|---|---|
| H5 | npm run build:h5 |
| 微信小程序 | npm run build:mp-weixin |
小程序构建内置 TDesign 适配补丁(
scripts/),构建成功后产物在dist/build/下。⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。
六、Mock 数据
- H5 开发模式(
NODE_ENV=development)默认关闭,全部走真实后端接口; - 开启方式:将
src/static/config.js的enableMock改为true(显式设置优先级最高),或通过uni.setStorageSync('enableMock', true)/ 环境变量MOCK_ENABLE切换; - 覆盖 200+ 接口,fixture 位于
src/mock/data/,按module/action路由镜像; - 未覆盖的接口自动透传真实网络并输出提示;
- Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。
七、目录结构
src/
├── pages/ 页面(index 主包 + public/app/user/order/course 五个分包,约 100+ 页面)
│ ├── index/ 首页(主包唯一页面,tabBar 入口)
│ ├── launch/ 启动页(加载与参数透传跳转,未在 pages.json 注册)
│ ├── public/ 公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等
│ ├── app/ 业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等
│ ├── user/ 用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等
│ ├── order/ 订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等
│ ├── course/ 课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等
│ └── template/ 页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)
├── components/ 组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)
├── common/ 公共层
│ ├── request/ 接口封装($api 点分路由、统一鉴权/错误处理)
│ ├── utils/ 工具(路由映射、支付、直播、OSS 签名等)
│ ├── order/ 订单状态处理
│ └── wechat/ 微信登录/分享 SDK 封装
├── mock/ Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)
├── static/ 静态资源
├── pages.json 页面路由与分包配置
└── manifest.json 应用配置(appid 等)
八、开发约定
- 页面采用
xxx.vue + js.vue + css.css三文件拆分(<script src="./js.vue">+@import "./css.css"),逻辑与样式分离; - 接口调用统一走
this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理; - 业务组件优先复用
components/tz/自研组件库; - 提交信息遵循 Conventional Commits(
feat:/fix:/chore:)。
九、相关链接
| 名称 | 链接 |
|---|---|
| 使用文档 | https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh |
| 功能列表 | https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe |
| 授权价格 | https://www.tuzhi.ltd/index/pricing/version.html |
| 功能演示 | https://www.tuzhi.ltd/index/demo/index.html |
十、软件安全
安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。
十一、使用许可
- 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE;
- 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);
仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open
凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。
凸知移动端 是一款基于 uni-app(Vue 2)+ TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。
系统演示:
| 端 | 演示地址 | 账号 | 密码 |
|---|---|---|---|
| 后台 | https://tuzhi.mutouweb.com/admin.php | admin | 123456 |
| 移动端(用户) | https://tuzhi.mutouweb.com | 13222222222 | 123456 |
| PC 端(用户) | https://tuzhi.mutouweb.com/pc/#/ | 13222222222 | 123456 |
一、功能特性
- 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
- 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
- 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
- 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
- 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
- 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
- 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
- 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
- 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)
界面预览:
二、系统架构
凸知是一套完整的知识付费系统,由四个部分组成:
| 部分 | 技术栈 | 说明 | 源码开放 |
|---|---|---|---|
| 后端服务 | ThinkPHP 5.0 + FastAdmin + MySQL | 接口服务,承载支付、直播、分销等全部业务逻辑 | 购买授权 |
| 移动端(本仓库) | uni-app(Vue 2)+ TDesign | H5 / 微信小程序学员端(不支持 App 打包) | 开源 |
| 管理后台 | Vue 3 + Arco Design | 管理员 / 讲师运营后台 | 购买授权 |
| PC 学员端 | Vue 3 + Arco Design | 电脑端学员学习页面 | 购买授权 |
凸知终端技术结构:
整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。
本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格。
三、技术栈
| 分类 | 技术 | 说明 |
|---|---|---|
| 跨端框架 | uni-app(Vue 2.x) | vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包) |
| 开发语言 | JavaScript(Vue 2 语法)+ Sass | 页面按 xxx.vue + js.vue + css.css 三文件拆分,样式用 CSS 规则 |
| UI 组件库 | TDesign(@tdesign/uniapp) |
全局主题样式,小程序端内置构建适配补丁(scripts/) |
uView(本地 components/uview-ui) |
列表 / 弹窗 / 表单等通用组件 | |
自研 components/tz/ |
音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45+ 业务组件 | |
| qiun-data-charts | 图表可视化 | |
| 状态管理 | Vuex 3 | 全局登录态 / 用户信息等 |
| 国际化 | vue-i18n 8 | 内置 zh-CN / US 多语言 |
| 网络请求 | flyio + 自封装 $api |
$api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转 |
| 音视频播放 | 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js | 直播与点播,支持倍速 / 弹幕 / 水印等组件 |
| 微信能力 | jweixin-module | 微信登录 / 分享 JS-SDK 封装 |
| 构建工具 | @vue/cli 5 + webpack | 由 @dcloudio/vue-cli-plugin-uni 驱动多端编译 |
| 测试 | Jest | 多端单元测试(npm run test:*) |
四、快速开始
环境要求:Node.js >= 14(建议 16/18)、npm
# ① 安装依赖
npm install
# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)
npm run dev:h5
浏览器访问 http://localhost:8080 即可看到完整页面效果。
微信小程序编译:
# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)
npm run dev:mp-weixin
# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)
npm run build:mp-weixin
用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。
对接真实后端:
- 修改
src/siteinfo.js中的siteroot为你的后端地址; - 在
src/common/request/api_list.js中核对接口地址与{code, msg, data}协议; - 微信小程序构建需在
src/manifest.json中替换为自己的 appid。
五、构建
| 目标 | 命令 |
|---|---|
| H5 | npm run build:h5 |
| 微信小程序 | npm run build:mp-weixin |
小程序构建内置 TDesign 适配补丁(
scripts/),构建成功后产物在dist/build/下。⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。
六、Mock 数据
- H5 开发模式(
NODE_ENV=development)默认关闭,全部走真实后端接口; - 开启方式:将
src/static/config.js的enableMock改为true(显式设置优先级最高),或通过uni.setStorageSync('enableMock', true)/ 环境变量MOCK_ENABLE切换; - 覆盖 200+ 接口,fixture 位于
src/mock/data/,按module/action路由镜像; - 未覆盖的接口自动透传真实网络并输出提示;
- Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。
七、目录结构
src/
├── pages/ 页面(index 主包 + public/app/user/order/course 五个分包,约 100+ 页面)
│ ├── index/ 首页(主包唯一页面,tabBar 入口)
│ ├── launch/ 启动页(加载与参数透传跳转,未在 pages.json 注册)
│ ├── public/ 公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等
│ ├── app/ 业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等
│ ├── user/ 用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等
│ ├── order/ 订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等
│ ├── course/ 课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等
│ └── template/ 页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)
├── components/ 组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)
├── common/ 公共层
│ ├── request/ 接口封装($api 点分路由、统一鉴权/错误处理)
│ ├── utils/ 工具(路由映射、支付、直播、OSS 签名等)
│ ├── order/ 订单状态处理
│ └── wechat/ 微信登录/分享 SDK 封装
├── mock/ Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)
├── static/ 静态资源
├── pages.json 页面路由与分包配置
└── manifest.json 应用配置(appid 等)
八、开发约定
- 页面采用
xxx.vue + js.vue + css.css三文件拆分(<script src="./js.vue">+@import "./css.css"),逻辑与样式分离; - 接口调用统一走
this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理; - 业务组件优先复用
components/tz/自研组件库; - 提交信息遵循 Conventional Commits(
feat:/fix:/chore:)。
九、相关链接
| 名称 | 链接 |
|---|---|
| 使用文档 | https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh |
| 功能列表 | https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe |
| 授权价格 | https://www.tuzhi.ltd/index/pricing/version.html |
| 功能演示 | https://www.tuzhi.ltd/index/demo/index.html |
十、软件安全
安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。
十一、使用许可
- 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE;
- 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);












