HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

等保测评wgt打包有高危漏洞!

应用打wgt升级包,使用压缩工具打开wgt文件后,在app-service.js中可以看到云服务空间的accessKey和secretKey等信息,被检测为高危漏洞

应用打wgt升级包,使用压缩工具打开wgt文件后,在app-service.js中可以看到云服务空间的accessKey和secretKey等信息,被检测为高危漏洞

App Store Connect API 密钥是什么,为什么推荐用它登录?

iOS

'''# App Store Connect API 密钥是什么,为什么推荐用它登录?

App Store Connect API 密钥是 Apple 提供的一种无密码登录凭据,通过 Issuer ID、Key ID 和一把私钥(.p8 文件)生成短期 JWT 令牌来认证访问 App Store Connect,不需要输入 Apple ID 密码,也不会触发双重验证,且长期有效——适合团队协作、多设备使用或接入自动化流程。

和账号密码登录相比的三个优势

对比项 账号密码登录 API 密钥登录
是否需要密码 需要 不需要
是否触发双重验证 会(首次) 不会
是否长期有效、可多端共用 依赖登录会话 是,生成一次可在任意设备导入使用

怎么获取

在「开心上架(AppUploader)」的「账号概览」页面:

  1. 用账号密码方式先登录一次,进入「App Store Connect API 密钥」区域。
  2. 点击「生成密钥」,填写密钥名称,提交后会生成一把 ADMIN 权限、可访问全部 App 的密钥。
  3. 私钥(.p8)只能获取这一次,务必立即复制或下载为 .json 密钥文件保存好。
  4. 之后可以直接拿这份 .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)」的「账号概览」页面:

  1. 用账号密码方式先登录一次,进入「App Store Connect API 密钥」区域。
  2. 点击「生成密钥」,填写密钥名称,提交后会生成一把 ADMIN 权限、可访问全部 App 的密钥。
  3. 私钥(.p8)只能获取这一次,务必立即复制或下载为 .json 密钥文件保存好。
  4. 之后可以直接拿这份 .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。

对接真实后端

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 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.jsenableMock 改为 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。

对接真实后端

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 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.jsenableMock 改为 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

收起阅读 »

这20块独立打包费,算是喂狗了。

App打包

排除20分钟,使用独立打包,还是等了接近20分钟。。

排除20分钟,使用独立打包,还是等了接近20分钟。。

App SSL Pinning 抓包怎么办?三种方法拿到明文

iOS

'''之前处理 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)
    地图选点(笔记标记地点 / 腾讯地图)
    市集电商(商品 / 订单 / 优惠券 / 秒杀拼团等)
    后台:笔记 / 频道 / 话题、会员、商城、站点品牌、系统消息
    系统消息、点点助手
继续阅读 »

演示地址

  • 笔记创建 / 编辑 / 发布、点赞评论回复、付费笔记
    私域直播(视频 / 语音、货架带货、公屏、体验倒计时、结束复盘)
    音视频聊天室(麦位、心跳、体验时长)
    IM:私信 / 群聊、直播与聊天室 邀请卡片
    音视频通话(TRTC)
    地图选点(笔记标记地点 / 腾讯地图)
    市集电商(商品 / 订单 / 优惠券 / 秒杀拼团等)
    后台:笔记 / 频道 / 话题、会员、商城、站点品牌、系统消息
    系统消息、点点助手
收起阅读 »

iOS 卡顿排查:三个问题快速定位性能瓶颈

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,但用户会感到明显卡顿。

真正该关注的是帧间隔的稳定性。单帧延迟的尖峰,才是卡顿的真实来源。高刷设备上尤其明显。

排查顺序

  1. 看帧延迟曲线,确认卡顿是周期性还是无规律
  2. 看主线程占用,判断是不是 CPU 瓶颈
  3. 看 GPU 利用率,判断是不是 GPU 瓶颈
  4. 针对具体环节优化,改完对比帧延迟分布,而不是只看 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,但用户会感到明显卡顿。

真正该关注的是帧间隔的稳定性。单帧延迟的尖峰,才是卡顿的真实来源。高刷设备上尤其明显。

排查顺序

  1. 看帧延迟曲线,确认卡顿是周期性还是无规律
  2. 看主线程占用,判断是不是 CPU 瓶颈
  3. 看 GPU 利用率,判断是不是 GPU 瓶颈
  4. 针对具体环节优化,改完对比帧延迟分布,而不是只看 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银行卡支付

Stripe

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。

对接真实后端

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 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.jsenableMock 改为 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。

对接真实后端

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 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.jsenableMock 改为 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(发布前请核对);
收起阅读 »