HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

怎么用 BeeWare(Briefcase)把 Python 应用打包成 iOS App 并上架

iOS

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

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 的三个特性:

  1. 返回一个新函数,不立即执行
  2. 支持参数预置(柯里化)
  3. 硬绑定:绑定后无法再被 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() 做了四件事:

  1. 创建一个新的空对象
  2. 将该对象的 [[Prototype]] 指向 Foo.prototype
  3. 以该对象为 this 执行 Foo
  4. 如果 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 赢了

这正是 bind polyfill 里必须判断 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

十、最佳实践

  1. 优先用箭头函数写回调,避免 this 丢失
  2. 对象方法用简写语法 foo() {},不要用箭头函数(除非确实想要外层 this)
  3. class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器 bind
  4. 全项目开启严格模式 / 使用 ESM,让 this 错误尽早暴露为 undefined 报错
  5. TypeScript 用户:开启 noImplicitThis,并善用显式 this 参数:
    function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ }  
  6. 不确定时,打印 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 的三个特性:

  1. 返回一个新函数,不立即执行
  2. 支持参数预置(柯里化)
  3. 硬绑定:绑定后无法再被 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() 做了四件事:

  1. 创建一个新的空对象
  2. 将该对象的 [[Prototype]] 指向 Foo.prototype
  3. 以该对象为 this 执行 Foo
  4. 如果 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 赢了

这正是 bind polyfill 里必须判断 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

十、最佳实践

  1. 优先用箭头函数写回调,避免 this 丢失
  2. 对象方法用简写语法 foo() {},不要用箭头函数(除非确实想要外层 this)
  3. class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器 bind
  4. 全项目开启严格模式 / 使用 ESM,让 this 错误尽早暴露为 undefined 报错
  5. TypeScript 用户:开启 noImplicitThis,并善用显式 this 参数:
    function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ }  
  6. 不确定时,打印 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

Appstore上传 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 多端可用

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.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。

对接真实后端:

  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.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(发布前请核对);
收起阅读 »

等保测评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.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。

对接真实后端:

  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.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(发布前请核对);
收起阅读 »

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