怎么用 BeeWare(Briefcase)把 Python 应用打包成 iOS App 并上架
'''BeeWare 走的是和 Flet 不一样的路子:它的 Toga 用的是各平台的原生控件,iOS 上就是真的 UIKit,不套 Flutter 或者 WebView。打包工具叫 Briefcase,briefcase create iOS 会生成一个 Xcode 工程,briefcase build iOS 编译,briefcase package iOS 出包。
因为最终还是 Xcode 在干活,这几条命令都得在 macOS 上跑。Windows 上 Briefcase 只能处理 Windows/Android 那些目标。这一步没有变通。
我的分工是:Python 代码在 Windows 上写,iOS 出包丢给 macOS 机器;证书、描述文件、IPA 上传用开心上架(AppUploader)在 Windows 上处理。
先说结论:哪一步真绕不开 Mac
| 环节 | 我实际怎么做的 |
|---|---|
| 注册 Bundle ID、签发证书、生成描述文件 | Windows 上做完 |
| 写 Python 代码 | Windows |
briefcase create / build / package iOS |
要 macOS + Xcode |
| 提交 App Store | Windows 上传 |
| 装到 iPhone 测试 | 扫码注册 UDID,再扫码装 |
说明一下:我用的签证书工具本身在 macOS 上也能用(网页版不挑系统,桌面客户端有 macOS 版)。表里"不需要 Mac"是指这一步不再需要 Xcode、钥匙串访问或 Transporter。
第一步:在 pyproject.toml 里定好 bundle
Briefcase 的配置全在 pyproject.toml 里。iOS 的包标识由 bundle 加 app 名拼出来:
[tool.briefcase]
project_name = "My App"
bundle = "com.yourcompany"
version = "1.0.0"
[tool.briefcase.app.myapp]
formal_name = "My App"
这样最终的 Bundle Identifier 就是 com.yourcompany.myapp。拼出来的这个值要和 Apple 注册的 Bundle ID、描述文件绑定的 App ID 完全一致——注意是拼出来的结果,不是 bundle 字段本身,这点第一次配的时候容易搞错。
第二步:在 Windows 上签出证书和描述文件
注册 Bundle ID:开心上架的「Bundle ID」页面点「新建 Bundle ID」,Identifier 填拼出来的那个完整标识。创建后改不了,所以先把 bundle 和 app 名定下来再注册。
签发证书:「证书」页面点「新建证书」,上架选发布签名证书,真机调试选开发签名证书。绑定上面的 Bundle ID,填名称邮箱,设一个 .p12 保护密码——导入 Mac 钥匙串时要输,服务器不保存它,忘了只能吊销重签。勾上「使用开心上架服务同步证书」的话,出包用的那台 Mac 直接登录下载就行。
生成描述文件:「描述文件」页面点「新建描述文件」,上架选 App Store 类型,内测选 Ad Hoc 并勾上测试设备,关联上面的 Bundle ID 和证书,提交后下载 .mobileprovision。
私钥和 CSR 都在你本机生成后封进 .p12,不用在 Mac 上走「钥匙串访问 → 从证书颁发机构请求证书」那一套——对只写 Python、平时压根不碰 Xcode 的人来说,这一步省下来挺值的。
第三步:在 macOS 上导入签名材料
security import dist.p12 -k ~/Library/Keychains/login.keychain-db -P "你的p12密码" -T /usr/bin/codesign
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp AppStore.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
第四步:生成工程、编译、出包
在 macOS 上依次跑:
briefcase create iOS
briefcase build iOS
briefcase package iOS
briefcase create iOS 会在 build/ 下生成一个 Xcode 工程。package 这一步会做签名,Briefcase 会让你选用哪个签名身份(identity),也可以用参数直接指定,具体参数名以 briefcase package iOS --help 输出的为准——Briefcase 各版本的命令行选项变动比较频繁。
签名这块有个实操建议:别让 Xcode 自动管理签名。生成的工程里如果开着 Automatically manage signing,Xcode 会跑去 Apple 那边替你申请新证书,而同类型证书一共就 3 个名额,几轮下来会占满。用 Xcode 打开生成的工程,把它关掉,手动选第二步那套证书和描述文件,再回来跑 briefcase package iOS。
改了 pyproject.toml 里跟原生工程相关的配置之后,记得重新 briefcase create iOS(或者带 -r 更新),不然生成的工程里还是旧配置。
第五步:把 IPA 传上去
拿到包之后就跟 Mac 无关了。我一般先在开心上架的「文件&提交」页面把 IPA 打开看一眼签名信息——Bundle ID 是不是拼出来的那个、描述文件类型对不对、证书有没有过期,确认了再点「提交 App Store」。
命令行版本,在 Windows 上直接跑:
appuploader-cli upload -f MyApp.ipa -u dev@example.com -p abcd-efgh-ijkl-mnop
-p 是 Apple 生成的 App 专用密码(xxxx-xxxx-xxxx-xxxx),不是 Apple ID 登录密码。也可以用 API 密钥:
appuploader-cli upload -f MyApp.ipa --api-key <KeyID> --api-issuer <IssuerID> --private-key Auth_Key.p8
密钥方式只收 .ipa,且 App 要先在 App Store Connect 里建好。
第六步:发内测包
「设备」页面点「注册设备」,二维码发出去让人扫上报 UDID(一年 100 台,免费账号 3 台)。回描述文件那边点「重建」,生成一份包含这些设备的 Ad Hoc 描述文件,拷到 Mac 上替换掉旧的。
重新 package 一次,然后在「文件&提交」页面生成二维码让人扫码装。
写在最后
BeeWare 这条路对纯 Python 背景的人最友好的地方,是你几乎不用学 Objective-C 或 Swift。但签名这一环是躲不掉的 Apple 规则,和你用什么语言无关——把证书和描述文件在 Windows 上准备好,Mac 那边就只剩三条 briefcase 命令。'''
'''BeeWare 走的是和 Flet 不一样的路子:它的 Toga 用的是各平台的原生控件,iOS 上就是真的 UIKit,不套 Flutter 或者 WebView。打包工具叫 Briefcase,briefcase create iOS 会生成一个 Xcode 工程,briefcase build iOS 编译,briefcase package iOS 出包。
因为最终还是 Xcode 在干活,这几条命令都得在 macOS 上跑。Windows 上 Briefcase 只能处理 Windows/Android 那些目标。这一步没有变通。
我的分工是:Python 代码在 Windows 上写,iOS 出包丢给 macOS 机器;证书、描述文件、IPA 上传用开心上架(AppUploader)在 Windows 上处理。
先说结论:哪一步真绕不开 Mac
| 环节 | 我实际怎么做的 |
|---|---|
| 注册 Bundle ID、签发证书、生成描述文件 | Windows 上做完 |
| 写 Python 代码 | Windows |
briefcase create / build / package iOS |
要 macOS + Xcode |
| 提交 App Store | Windows 上传 |
| 装到 iPhone 测试 | 扫码注册 UDID,再扫码装 |
说明一下:我用的签证书工具本身在 macOS 上也能用(网页版不挑系统,桌面客户端有 macOS 版)。表里"不需要 Mac"是指这一步不再需要 Xcode、钥匙串访问或 Transporter。
第一步:在 pyproject.toml 里定好 bundle
Briefcase 的配置全在 pyproject.toml 里。iOS 的包标识由 bundle 加 app 名拼出来:
[tool.briefcase]
project_name = "My App"
bundle = "com.yourcompany"
version = "1.0.0"
[tool.briefcase.app.myapp]
formal_name = "My App"
这样最终的 Bundle Identifier 就是 com.yourcompany.myapp。拼出来的这个值要和 Apple 注册的 Bundle ID、描述文件绑定的 App ID 完全一致——注意是拼出来的结果,不是 bundle 字段本身,这点第一次配的时候容易搞错。
第二步:在 Windows 上签出证书和描述文件
注册 Bundle ID:开心上架的「Bundle ID」页面点「新建 Bundle ID」,Identifier 填拼出来的那个完整标识。创建后改不了,所以先把 bundle 和 app 名定下来再注册。
签发证书:「证书」页面点「新建证书」,上架选发布签名证书,真机调试选开发签名证书。绑定上面的 Bundle ID,填名称邮箱,设一个 .p12 保护密码——导入 Mac 钥匙串时要输,服务器不保存它,忘了只能吊销重签。勾上「使用开心上架服务同步证书」的话,出包用的那台 Mac 直接登录下载就行。
生成描述文件:「描述文件」页面点「新建描述文件」,上架选 App Store 类型,内测选 Ad Hoc 并勾上测试设备,关联上面的 Bundle ID 和证书,提交后下载 .mobileprovision。
私钥和 CSR 都在你本机生成后封进 .p12,不用在 Mac 上走「钥匙串访问 → 从证书颁发机构请求证书」那一套——对只写 Python、平时压根不碰 Xcode 的人来说,这一步省下来挺值的。
第三步:在 macOS 上导入签名材料
security import dist.p12 -k ~/Library/Keychains/login.keychain-db -P "你的p12密码" -T /usr/bin/codesign
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp AppStore.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
第四步:生成工程、编译、出包
在 macOS 上依次跑:
briefcase create iOS
briefcase build iOS
briefcase package iOS
briefcase create iOS 会在 build/ 下生成一个 Xcode 工程。package 这一步会做签名,Briefcase 会让你选用哪个签名身份(identity),也可以用参数直接指定,具体参数名以 briefcase package iOS --help 输出的为准——Briefcase 各版本的命令行选项变动比较频繁。
签名这块有个实操建议:别让 Xcode 自动管理签名。生成的工程里如果开着 Automatically manage signing,Xcode 会跑去 Apple 那边替你申请新证书,而同类型证书一共就 3 个名额,几轮下来会占满。用 Xcode 打开生成的工程,把它关掉,手动选第二步那套证书和描述文件,再回来跑 briefcase package iOS。
改了 pyproject.toml 里跟原生工程相关的配置之后,记得重新 briefcase create iOS(或者带 -r 更新),不然生成的工程里还是旧配置。
第五步:把 IPA 传上去
拿到包之后就跟 Mac 无关了。我一般先在开心上架的「文件&提交」页面把 IPA 打开看一眼签名信息——Bundle ID 是不是拼出来的那个、描述文件类型对不对、证书有没有过期,确认了再点「提交 App Store」。
命令行版本,在 Windows 上直接跑:
appuploader-cli upload -f MyApp.ipa -u dev@example.com -p abcd-efgh-ijkl-mnop
-p 是 Apple 生成的 App 专用密码(xxxx-xxxx-xxxx-xxxx),不是 Apple ID 登录密码。也可以用 API 密钥:
appuploader-cli upload -f MyApp.ipa --api-key <KeyID> --api-issuer <IssuerID> --private-key Auth_Key.p8
密钥方式只收 .ipa,且 App 要先在 App Store Connect 里建好。
第六步:发内测包
「设备」页面点「注册设备」,二维码发出去让人扫上报 UDID(一年 100 台,免费账号 3 台)。回描述文件那边点「重建」,生成一份包含这些设备的 Ad Hoc 描述文件,拷到 Mac 上替换掉旧的。
重新 package 一次,然后在「文件&提交」页面生成二维码让人扫码装。
写在最后
BeeWare 这条路对纯 Python 背景的人最友好的地方,是你几乎不用学 Objective-C 或 Swift。但签名这一环是躲不掉的 Apple 规则,和你用什么语言无关——把证书和描述文件在 Windows 上准备好,Mac 那边就只剩三条 briefcase 命令。'''
收起阅读 »JavaScript `this` 指南
JavaScript this 指南
一、核心心法
this不是在函数定义时确定的,而是在函数被调用时确定的。
判断this,只需要看一件事:这个函数是怎么被调用的(调用点)。
唯一的例外是箭头函数——它根本没有自己的 this。
二、五种绑定规则
规则 1:默认绑定(独立函数调用)
function foo() {
console.log(this);
}
foo(); // 非严格模式: globalThis (浏览器 window / Node global)
// 严格模式: undefined
'use strict';
function foo() { console.log(this); }
foo(); // undefined
顶层 this 的值取决于环境:
| 环境 | 顶层 this |
|---|---|
浏览器 <script> |
window |
ES Module (.mjs / type="module") |
undefined |
CommonJS 模块 (Node .js) |
module.exports(即 {}) |
| 函数内(非严格) | globalThis |
| 函数内(严格) | undefined |
| class 内部 | 永远严格模式 |
规则 2:隐式绑定(方法调用)
看调用时最后一个点前面是谁。
const obj = {
name: 'obj',
foo() { console.log(this.name); }
};
obj.foo(); // 'obj'
多层嵌套只看最后一层:
const a = { name: 'a', b: { name: 'b', foo() { console.log(this.name); } } };
a.b.foo(); // 'b' ← 不是 'a'
方括号访问同理:
a.b['foo'](); // 'b'
规则 3:显式绑定(call / apply / bind)
function greet(greeting, punct) {
console.log(greeting + ', ' + this.name + punct);
}
const me = { name: 'Alice' };
greet.call(me, 'Hi', '!'); // 逐个传参
greet.apply(me, ['Hi', '!']); // 数组传参 (Apply → Array)
const bound = greet.bind(me, 'Hi'); // 返回新函数 + 支持柯里化
bound('!');
bind 的三个特性:
- 返回一个新函数,不立即执行
- 支持参数预置(柯里化)
- 硬绑定:绑定后无法再被
call/apply改变
const hard = greet.bind(me);
hard.call({ name: 'Bob' }, 'Yo', '!'); // 依然是 Alice
传 null / undefined 的坑:
function foo() { console.log(this); }
foo.call(null); // 非严格: globalThis(被替换了!) 严格: null
安全做法——DMZ 空对象:
const DMZ = Object.create(null); // 比 {} 更干净,无原型
foo.apply(DMZ, [1, 2]);
原始值会被装箱(仅非严格模式):
function f() { console.log(typeof this); }
f.call(1); // 'object' (Number 包装对象)
// 严格模式下: 'number'
规则 4:new 绑定
new Foo() 做了四件事:
- 创建一个新的空对象
- 将该对象的
[[Prototype]]指向Foo.prototype - 以该对象为
this执行Foo - 如果
Foo没有返回一个对象,则返回这个新对象
function Person(name) {
this.name = name;
// return { name: 'hacked' }; // 若返回对象,则 new 的结果是它
// return 42; // 返回原始值 → 被忽略,仍返回 this
}
const p = new Person('Alice'); // p.name === 'Alice'
规则 5:箭头函数(词法绑定)
箭头函数没有自己的 this,它继承定义时所在外层作用域的 this,且永久不可改变。
const obj = {
name: 'obj',
regular() {
const arrow = () => console.log(this.name);
arrow(); // 'obj' ← 继承 regular 的 this
},
arrowMethod: () => console.log(this?.name) // ⚠️ 继承的是模块/全局的 this
};
obj.regular(); // 'obj'
obj.arrowMethod(); // undefined ← 经典陷阱!
箭头函数无法被改变:
const arrow = () => console.log(this);
arrow.call({ a: 1 }); // call 无效
arrow.bind({ a: 1 })(); // bind 无效
new arrow(); // TypeError: arrow is not a constructor
箭头函数也没有 arguments、super、new.target。
三、优先级
new 绑定 > 显式绑定(bind/call/apply) > 隐式绑定(obj.fn) > 默认绑定
箭头函数不在此列——它压根不参与,直接看词法作用域。
验证 new > bind:
function Foo(a) { this.a = a; }
const obj = {};
const B = Foo.bind(obj);
B(2); // obj.a === 2 (bind 生效)
const b = new B(3);// b.a === 3, obj.a 仍是 2 ← new 赢了
这正是
bindpolyfill 里必须判断new.target的原因。
四、隐式丢失:最常见的 Bug 来源
1. 赋值给变量
const obj = { name: 'obj', foo() { console.log(this?.name); } };
const fn = obj.foo; // 只拿到了函数引用,丢掉了 obj
fn(); // undefined
2. 作为回调传递
setTimeout(obj.foo, 100); // 丢失
[1].forEach(obj.foo); // 丢失
btn.addEventListener('click', obj.foo); // this 变成 btn
3. 解构
const { foo } = obj;
foo(); // 丢失
这就是 React 中
const { setState } = this会出问题、
以及 Vue3 用ref而非this的部分原因。
4. 间接引用
const o1 = { name: 'o1', foo() { console.log(this.name); } };
const o2 = { name: 'o2' };
o2.foo = o1.foo;
o2.foo(); // 'o2' (正常隐式绑定)
(o2.foo = o1.foo)(); // 赋值表达式返回函数本身 → 默认绑定 → undefined/报错
5. 解决方案汇总
// ① bind
setTimeout(obj.foo.bind(obj), 100);
// ② 箭头函数包裹(推荐,可读性好)
setTimeout(() => obj.foo(), 100);
// ③ 数组方法的 thisArg 参数
[1, 2].forEach(obj.foo, obj);
// ④ class field 箭头函数(见下文)
支持 thisArg 的数组方法:
forEach map filter some every find findIndex findLast findLastIndex flatMap
不支持的: reduce reduceRight sort(它们的第二参数另有用途)
五、Class 中的 this
类体内永远是严格模式
class Counter {
count = 0;
inc() { this.count++; } // 原型方法 → this 会丢失
dec = () => { this.count--; }; // 类字段箭头函数 → this 永久绑定实例
}
const c = new Counter();
const { inc, dec } = c;
dec(); // ✅ 正常
inc(); // ❌ TypeError: Cannot read properties of undefined
两者的本质区别:
原型方法 inc() |
类字段 dec = () => {} |
|
|---|---|---|
| 存放位置 | Counter.prototype |
每个实例上 |
| 内存 | 共享一份 | 每个实例一份(内存开销大) |
this |
调用时决定 | 定义时锁死为实例 |
可被 call 改变 |
是 | 否 |
可被子类 super 调用 |
是 | 否 |
推荐做法: 默认用原型方法,只在需要作为回调传递时用箭头字段,或在构造器中 bind:
class Counter {
constructor() {
this.inc = this.inc.bind(this); // 等价效果
}
}
静态方法中的 this
class A {
static create() { return new this(); } // this === 类本身,支持子类继承
}
class B extends A {}
B.create(); // 返回 B 的实例,而非 A
继承与 super
class Base {
constructor() { this.type = 'base'; }
hello() { console.log('base hello'); }
}
class Child extends Base {
constructor() {
// console.log(this); // ❌ ReferenceError: 必须先调用 super()
super(); // 派生类中,super() 之前 this 处于 TDZ
console.log(this.type);// 'base'
}
hello() {
super.hello(); // super 方法调用时,this 仍是当前实例
}
}
私有字段的坑
class A {
#secret = 1;
get() { return this.#secret; }
}
const a = new A();
a.get.call({}); // TypeError: Cannot read private member #secret
// 私有字段是"品牌检查",this 必须是真实实例
六、各类场景速查
DOM 事件
btn.addEventListener('click', function () {
console.log(this); // === event.currentTarget (btn)
});
btn.addEventListener('click', () => {
console.log(this); // 外层作用域的 this,不是 btn
});
handleEvent 特殊接口(传对象而非函数):
const handler = {
count: 0,
handleEvent(e) { console.log(this.count++, e.type); } // this === handler
};
btn.addEventListener('click', handler);
内联 onclick="..." 属性中的 this 也是元素本身。
定时器
setTimeout(function () {
console.log(this);
// 浏览器非严格: window
// Node: Timeout 对象
}, 0);
getter / setter
const obj = {
_v: 1,
get v() { return this._v; } // this 是访问该属性的对象
};
Proxy 中的 this 泄漏
const target = { greet() { return this; } };
const proxy = new Proxy(target, {});
proxy.greet() === proxy; // true —— this 是 proxy,不是 target
// 若 target 用了私有字段或内部槽(如 Map/Set),会报错
标签模板 / 生成器 / async
async function f() { return this; } // 与普通函数规则一致
function* g() { yield this; } // 同上
七、手写实现(面试高频)
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') throw new TypeError('not a function');
context = context ?? globalThis;
if (Object(context) !== context) context = Object(context); // 原始值装箱
const key = Symbol('fn'); // Symbol 避免属性名冲突
context[key] = this;
try {
return context[key](...args);
} finally {
delete context[key];
}
};
Function.prototype.myApply = function (context, args = []) {
return this.myCall(context, ...args);
};
Function.prototype.myBind = function (context, ...preArgs) {
const fn = this;
if (typeof fn !== 'function') throw new TypeError('not a function');
function bound(...args) {
// 关键:new 调用时忽略 context,使用新建的 this
return fn.apply(
new.target ? this : context,
[...preArgs, ...args]
);
}
// 维持原型链,使 instanceof 正确
bound.prototype = Object.create(fn.prototype || null);
return bound;
};
// 手写 new
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype);
const ret = Ctor.apply(obj, args);
return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))
? ret
: obj;
}
八、综合练习(先自己作答)
var name = 'global'; // 注意:必须是 var,let/const 不挂载到 window
const obj = {
name: 'obj',
f1() { console.log(this.name); },
f2: () => console.log(this?.name),
f3() {
return function () { console.log(this?.name); };
},
f4() {
return () => console.log(this.name);
}
};
obj.f1(); // ?
obj.f2(); // ?
obj.f3()(); // ?
obj.f4()(); // ?
const g = obj.f1;
g(); // ?
obj.f1.call({ name: 'x' }); // ?
new obj.f1(); // ?
答案
obj.f1() → 'obj' 隐式绑定
obj.f2() → 'global'/undefined 箭头函数继承顶层 this
obj.f3()() → 'global'/undefined 返回的函数独立调用,默认绑定
obj.f4()() → 'obj' 箭头函数继承 f4 的 this
g() → 'global'/undefined 隐式丢失
obj.f1.call({...})→ 'x' 显式绑定
new obj.f1() → undefined new 绑定,新对象上没有 name
(浏览器非严格模式下顶层 this 是 window,故为 'global';ESM/严格模式下为 undefined 或报错)
九、终极判断流程图
函数是箭头函数?
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)
└─ 否 ↓
用 new 调用? ──→ 是 → this = 新创建的对象
↓ 否
用 call/apply/bind 调用? ──→ 是 → this = 指定的对象
↓ 否 (非严格下 null/undefined 会替换为 globalThis)
通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)
↓ 否
严格模式? ──→ 是 → this = undefined
└→ 否 → this = globalThis
十、最佳实践
- 优先用箭头函数写回调,避免
this丢失 - 对象方法用简写语法
foo() {},不要用箭头函数(除非确实想要外层this) - class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器
bind - 全项目开启严格模式 / 使用 ESM,让
this错误尽早暴露为undefined报错 - TypeScript 用户:开启
noImplicitThis,并善用显式this参数:function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ } - 不确定时,打印
console.log(this)—— 比推理快十倍
JavaScript this 指南
一、核心心法
this不是在函数定义时确定的,而是在函数被调用时确定的。
判断this,只需要看一件事:这个函数是怎么被调用的(调用点)。
唯一的例外是箭头函数——它根本没有自己的 this。
二、五种绑定规则
规则 1:默认绑定(独立函数调用)
function foo() {
console.log(this);
}
foo(); // 非严格模式: globalThis (浏览器 window / Node global)
// 严格模式: undefined
'use strict';
function foo() { console.log(this); }
foo(); // undefined
顶层 this 的值取决于环境:
| 环境 | 顶层 this |
|---|---|
浏览器 <script> |
window |
ES Module (.mjs / type="module") |
undefined |
CommonJS 模块 (Node .js) |
module.exports(即 {}) |
| 函数内(非严格) | globalThis |
| 函数内(严格) | undefined |
| class 内部 | 永远严格模式 |
规则 2:隐式绑定(方法调用)
看调用时最后一个点前面是谁。
const obj = {
name: 'obj',
foo() { console.log(this.name); }
};
obj.foo(); // 'obj'
多层嵌套只看最后一层:
const a = { name: 'a', b: { name: 'b', foo() { console.log(this.name); } } };
a.b.foo(); // 'b' ← 不是 'a'
方括号访问同理:
a.b['foo'](); // 'b'
规则 3:显式绑定(call / apply / bind)
function greet(greeting, punct) {
console.log(greeting + ', ' + this.name + punct);
}
const me = { name: 'Alice' };
greet.call(me, 'Hi', '!'); // 逐个传参
greet.apply(me, ['Hi', '!']); // 数组传参 (Apply → Array)
const bound = greet.bind(me, 'Hi'); // 返回新函数 + 支持柯里化
bound('!');
bind 的三个特性:
- 返回一个新函数,不立即执行
- 支持参数预置(柯里化)
- 硬绑定:绑定后无法再被
call/apply改变
const hard = greet.bind(me);
hard.call({ name: 'Bob' }, 'Yo', '!'); // 依然是 Alice
传 null / undefined 的坑:
function foo() { console.log(this); }
foo.call(null); // 非严格: globalThis(被替换了!) 严格: null
安全做法——DMZ 空对象:
const DMZ = Object.create(null); // 比 {} 更干净,无原型
foo.apply(DMZ, [1, 2]);
原始值会被装箱(仅非严格模式):
function f() { console.log(typeof this); }
f.call(1); // 'object' (Number 包装对象)
// 严格模式下: 'number'
规则 4:new 绑定
new Foo() 做了四件事:
- 创建一个新的空对象
- 将该对象的
[[Prototype]]指向Foo.prototype - 以该对象为
this执行Foo - 如果
Foo没有返回一个对象,则返回这个新对象
function Person(name) {
this.name = name;
// return { name: 'hacked' }; // 若返回对象,则 new 的结果是它
// return 42; // 返回原始值 → 被忽略,仍返回 this
}
const p = new Person('Alice'); // p.name === 'Alice'
规则 5:箭头函数(词法绑定)
箭头函数没有自己的 this,它继承定义时所在外层作用域的 this,且永久不可改变。
const obj = {
name: 'obj',
regular() {
const arrow = () => console.log(this.name);
arrow(); // 'obj' ← 继承 regular 的 this
},
arrowMethod: () => console.log(this?.name) // ⚠️ 继承的是模块/全局的 this
};
obj.regular(); // 'obj'
obj.arrowMethod(); // undefined ← 经典陷阱!
箭头函数无法被改变:
const arrow = () => console.log(this);
arrow.call({ a: 1 }); // call 无效
arrow.bind({ a: 1 })(); // bind 无效
new arrow(); // TypeError: arrow is not a constructor
箭头函数也没有 arguments、super、new.target。
三、优先级
new 绑定 > 显式绑定(bind/call/apply) > 隐式绑定(obj.fn) > 默认绑定
箭头函数不在此列——它压根不参与,直接看词法作用域。
验证 new > bind:
function Foo(a) { this.a = a; }
const obj = {};
const B = Foo.bind(obj);
B(2); // obj.a === 2 (bind 生效)
const b = new B(3);// b.a === 3, obj.a 仍是 2 ← new 赢了
这正是
bindpolyfill 里必须判断new.target的原因。
四、隐式丢失:最常见的 Bug 来源
1. 赋值给变量
const obj = { name: 'obj', foo() { console.log(this?.name); } };
const fn = obj.foo; // 只拿到了函数引用,丢掉了 obj
fn(); // undefined
2. 作为回调传递
setTimeout(obj.foo, 100); // 丢失
[1].forEach(obj.foo); // 丢失
btn.addEventListener('click', obj.foo); // this 变成 btn
3. 解构
const { foo } = obj;
foo(); // 丢失
这就是 React 中
const { setState } = this会出问题、
以及 Vue3 用ref而非this的部分原因。
4. 间接引用
const o1 = { name: 'o1', foo() { console.log(this.name); } };
const o2 = { name: 'o2' };
o2.foo = o1.foo;
o2.foo(); // 'o2' (正常隐式绑定)
(o2.foo = o1.foo)(); // 赋值表达式返回函数本身 → 默认绑定 → undefined/报错
5. 解决方案汇总
// ① bind
setTimeout(obj.foo.bind(obj), 100);
// ② 箭头函数包裹(推荐,可读性好)
setTimeout(() => obj.foo(), 100);
// ③ 数组方法的 thisArg 参数
[1, 2].forEach(obj.foo, obj);
// ④ class field 箭头函数(见下文)
支持 thisArg 的数组方法:
forEach map filter some every find findIndex findLast findLastIndex flatMap
不支持的: reduce reduceRight sort(它们的第二参数另有用途)
五、Class 中的 this
类体内永远是严格模式
class Counter {
count = 0;
inc() { this.count++; } // 原型方法 → this 会丢失
dec = () => { this.count--; }; // 类字段箭头函数 → this 永久绑定实例
}
const c = new Counter();
const { inc, dec } = c;
dec(); // ✅ 正常
inc(); // ❌ TypeError: Cannot read properties of undefined
两者的本质区别:
原型方法 inc() |
类字段 dec = () => {} |
|
|---|---|---|
| 存放位置 | Counter.prototype |
每个实例上 |
| 内存 | 共享一份 | 每个实例一份(内存开销大) |
this |
调用时决定 | 定义时锁死为实例 |
可被 call 改变 |
是 | 否 |
可被子类 super 调用 |
是 | 否 |
推荐做法: 默认用原型方法,只在需要作为回调传递时用箭头字段,或在构造器中 bind:
class Counter {
constructor() {
this.inc = this.inc.bind(this); // 等价效果
}
}
静态方法中的 this
class A {
static create() { return new this(); } // this === 类本身,支持子类继承
}
class B extends A {}
B.create(); // 返回 B 的实例,而非 A
继承与 super
class Base {
constructor() { this.type = 'base'; }
hello() { console.log('base hello'); }
}
class Child extends Base {
constructor() {
// console.log(this); // ❌ ReferenceError: 必须先调用 super()
super(); // 派生类中,super() 之前 this 处于 TDZ
console.log(this.type);// 'base'
}
hello() {
super.hello(); // super 方法调用时,this 仍是当前实例
}
}
私有字段的坑
class A {
#secret = 1;
get() { return this.#secret; }
}
const a = new A();
a.get.call({}); // TypeError: Cannot read private member #secret
// 私有字段是"品牌检查",this 必须是真实实例
六、各类场景速查
DOM 事件
btn.addEventListener('click', function () {
console.log(this); // === event.currentTarget (btn)
});
btn.addEventListener('click', () => {
console.log(this); // 外层作用域的 this,不是 btn
});
handleEvent 特殊接口(传对象而非函数):
const handler = {
count: 0,
handleEvent(e) { console.log(this.count++, e.type); } // this === handler
};
btn.addEventListener('click', handler);
内联 onclick="..." 属性中的 this 也是元素本身。
定时器
setTimeout(function () {
console.log(this);
// 浏览器非严格: window
// Node: Timeout 对象
}, 0);
getter / setter
const obj = {
_v: 1,
get v() { return this._v; } // this 是访问该属性的对象
};
Proxy 中的 this 泄漏
const target = { greet() { return this; } };
const proxy = new Proxy(target, {});
proxy.greet() === proxy; // true —— this 是 proxy,不是 target
// 若 target 用了私有字段或内部槽(如 Map/Set),会报错
标签模板 / 生成器 / async
async function f() { return this; } // 与普通函数规则一致
function* g() { yield this; } // 同上
七、手写实现(面试高频)
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') throw new TypeError('not a function');
context = context ?? globalThis;
if (Object(context) !== context) context = Object(context); // 原始值装箱
const key = Symbol('fn'); // Symbol 避免属性名冲突
context[key] = this;
try {
return context[key](...args);
} finally {
delete context[key];
}
};
Function.prototype.myApply = function (context, args = []) {
return this.myCall(context, ...args);
};
Function.prototype.myBind = function (context, ...preArgs) {
const fn = this;
if (typeof fn !== 'function') throw new TypeError('not a function');
function bound(...args) {
// 关键:new 调用时忽略 context,使用新建的 this
return fn.apply(
new.target ? this : context,
[...preArgs, ...args]
);
}
// 维持原型链,使 instanceof 正确
bound.prototype = Object.create(fn.prototype || null);
return bound;
};
// 手写 new
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype);
const ret = Ctor.apply(obj, args);
return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))
? ret
: obj;
}
八、综合练习(先自己作答)
var name = 'global'; // 注意:必须是 var,let/const 不挂载到 window
const obj = {
name: 'obj',
f1() { console.log(this.name); },
f2: () => console.log(this?.name),
f3() {
return function () { console.log(this?.name); };
},
f4() {
return () => console.log(this.name);
}
};
obj.f1(); // ?
obj.f2(); // ?
obj.f3()(); // ?
obj.f4()(); // ?
const g = obj.f1;
g(); // ?
obj.f1.call({ name: 'x' }); // ?
new obj.f1(); // ?
答案
obj.f1() → 'obj' 隐式绑定
obj.f2() → 'global'/undefined 箭头函数继承顶层 this
obj.f3()() → 'global'/undefined 返回的函数独立调用,默认绑定
obj.f4()() → 'obj' 箭头函数继承 f4 的 this
g() → 'global'/undefined 隐式丢失
obj.f1.call({...})→ 'x' 显式绑定
new obj.f1() → undefined new 绑定,新对象上没有 name
(浏览器非严格模式下顶层 this 是 window,故为 'global';ESM/严格模式下为 undefined 或报错)
九、终极判断流程图
函数是箭头函数?
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)
└─ 否 ↓
用 new 调用? ──→ 是 → this = 新创建的对象
↓ 否
用 call/apply/bind 调用? ──→ 是 → this = 指定的对象
↓ 否 (非严格下 null/undefined 会替换为 globalThis)
通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)
↓ 否
严格模式? ──→ 是 → this = undefined
└→ 否 → this = globalThis
十、最佳实践
- 优先用箭头函数写回调,避免
this丢失 - 对象方法用简写语法
foo() {},不要用箭头函数(除非确实想要外层this) - class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器
bind - 全项目开启严格模式 / 使用 ESM,让
this错误尽早暴露为undefined报错 - TypeScript 用户:开启
noImplicitThis,并善用显式this参数:function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ } - 不确定时,打印
console.log(this)—— 比推理快十倍
Android离线打包,plus.runtime.install导致应用闪退
logcat日志如下:
Unresolved exception class when finding catch block: net.lingala.zip4j.exception.ZipException
2026-08-28 15:40:18.591 29792-30261 AndroidRuntime com.gdxinyue.newusmp
E FATAL EXCEPTION: Thread-10
Process: com.gdxinyue.newusmp, PID: 29792
java.lang.NullPointerException: Attempt to read from null array
at io.dcloud.feature.pdr.RuntimeFeatureImpl$a.run
参考博客:https://blog.csdn.net/qq_32058147/article/details/155034674
在Android Stadio主项目的 build.gradle > dependencies中增加以下依赖:
dependencies {
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}
logcat日志如下:
Unresolved exception class when finding catch block: net.lingala.zip4j.exception.ZipException
2026-08-28 15:40:18.591 29792-30261 AndroidRuntime com.gdxinyue.newusmp
E FATAL EXCEPTION: Thread-10
Process: com.gdxinyue.newusmp, PID: 29792
java.lang.NullPointerException: Attempt to read from null array
at io.dcloud.feature.pdr.RuntimeFeatureImpl$a.run
参考博客:https://blog.csdn.net/qq_32058147/article/details/155034674
在Android Stadio主项目的 build.gradle > dependencies中增加以下依赖:
dependencies {
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}
uni-starter模板代码更正
创建uni-app项目时选择uni-starter模板,我发现其中pages/list/search/search.vue中的“搜索联想”部分代码<uni-list-item>的:title="item.name"可能写错了,应该改为:title="item.title",具体代码如下:
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<!-- :title="item.name"可能写错了,文章表没有name字段,associativeList只是获取_id、title字段! -->
<!-- <uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.name" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item> -->
<!-- 正确改为:title="item.title",在搜索框输入文章记录的标题子串时,会显示篇文章的标题 -->
<uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item>
</uni-list>
</view>
这个搜索联想的功能应该是在搜索框输入文章记录标题的子串时,提示有没有这篇文章,若存在,则显示所有标题带有此子串的文章标题列表,这样就可以直接点击找到此文章,因为模板的代码写错了"item.name",导致完全没有提示,希望可以改正过来。
创建uni-app项目时选择uni-starter模板,我发现其中pages/list/search/search.vue中的“搜索联想”部分代码<uni-list-item>的:title="item.name"可能写错了,应该改为:title="item.title",具体代码如下:
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<!-- :title="item.name"可能写错了,文章表没有name字段,associativeList只是获取_id、title字段! -->
<!-- <uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.name" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item> -->
<!-- 正确改为:title="item.title",在搜索框输入文章记录的标题子串时,会显示篇文章的标题 -->
<uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item>
</uni-list>
</view>
这个搜索联想的功能应该是在搜索框输入文章记录标题的子串时,提示有没有这篇文章,若存在,则显示所有标题带有此子串的文章标题列表,这样就可以直接点击找到此文章,因为模板的代码写错了"item.name",导致完全没有提示,希望可以改正过来。
收起阅读 »无Mac获取P12证书、描述文件、上传IPA
1、首先进入我开发的网站:登录 - iOS 开发者工具,http://jqzm.w1.luyouxia.net/iOSTools,该工具集成了Bundle Id、证书、描述文件、添加设备、上传IPA
2、然后使用邮箱注册并登陆
3、然后进入账号设置,将参数填写上并保存配置,保存后点一下同步数据
4、进入上传IPA功能,选择IPA文件上传即可
1、首先进入我开发的网站:登录 - iOS 开发者工具,http://jqzm.w1.luyouxia.net/iOSTools,该工具集成了Bundle Id、证书、描述文件、添加设备、上传IPA
2、然后使用邮箱注册并登陆
3、然后进入账号设置,将参数填写上并保存配置,保存后点一下同步数据
4、进入上传IPA功能,选择IPA文件上传即可
收起阅读 »在线教育知识付费直播源码免费下载|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(发布前请核对);
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(发布前请核对);
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)
地图选点(笔记标记地点 / 腾讯地图)
市集电商(商品 / 订单 / 优惠券 / 秒杀拼团等)
后台:笔记 / 频道 / 话题、会员、商城、站点品牌、系统消息
系统消息、点点助手












