开源邀请归因系统 share-installs:自托管延迟深度链接匹配方案

前言 你有没有遇到过这种场景: 用户 A 分享了一个邀请链接给用户 B。用户 B 在手机上点击链接,跳转到 App Store 下载了 App。安装打开后——邀请码呢? 传统方案要么依赖复杂的 Deep Link 配置(Universal Link / App Links),要么需要接入 Firebase Dynamic Links 这类第三方服务。前者配置繁琐,后者有被 deprecate 的风险。 这就是我开源 share-installs 的原因。 什么是 share-installs? share-installs 是一个开源、自托管的 邀请码 & 延迟深度链接归因系统。 核心能力只有一句话:用户点击邀请链接并安装 App 后,首次启动时邀请码会自动识别并回填——即使点击时 App 尚未安装。 项目地址:github.com/ceeyang/share-installs ⭐ 核心功能:浏览器指纹 → 后端匹配 → App 启动自动回填 🖥️ 三端 SDK:Web(TypeScript)+ iOS(Swift)+ Android(Kotlin) 🐳 一键部署:Docker Compose / Kubernetes / 裸机 🔐 自托管:数据存在你自己的服务器上 工作原理 整个流程分为四个阶段: [阶段 1] 用户 A 分享邀请链接 │ ▼ [阶段 2] 用户 B 点击链接 → 你的落地页 │ JS SDK 上报浏览器指纹到后端 ▼ [阶段 3] 跳转 App Store / Play Store │ ▼ [阶段 4] 用户 B 安装并打开 App │ 移动 SDK 上报设备指纹 ▼ 后端匹配指纹 → 返回邀请码 │ ▼ App 自动填入邀请码 ✓ 关键技术点: ...

2026-06-01 · 3 min · 491 words · Cee Yang

2021国庆旅游攻略

2021国庆旅行计划 到成都南约50分钟,入站等需要提前一个半小时,总约 1.5 小时 到成都东约70分钟,入站等需要提前一个半小时,总约 2 小时 到犀浦站约100分钟,入站等需要提前一个半小时,总约 2.5 小时 成都南到峨眉山无票 成都东到峨眉山 1.5 个小时 成都东 11:00 ~ 12:23,需 9 点出门 成都东 12:11 ~ 13:34,需 10 点出门 * 成都东 13:15 ~ 14:18,需 11 点出门 * 成都东 13:50 ~ 15:17,需 11:30 点出门 犀浦到都江堰 30 分钟 峨眉山计划: 报国寺-清音寺/万年寺-雷洞坪-索道-金顶-日出/云海 09:00 ~ 11:00 点出门前往成都东站 11:00 ~ 12:23 成都东出发 到 达峨眉山站 12:30 ~ 13:00 午饭时间 13:00 ~ 14:00 乘坐公共交通到达报国寺车站 14:00 ~ 14:30 报国寺游玩儿 万年寺路线 14:30 ~ 15:30 观光车到达万年寺 15:30 ~ 16:00 万年寺,万年索道 14:20 ~ 16:30 到达雷洞坪, 2.0小时 16:30 ~ 17:30 入住酒店,雷洞坪购物街, 1.0 小时 17:30 ~ 19:30 晚饭时间, 1.0 小时 19:30 ~ 晚上早点休息, 次日需凌晨 4:30 起床看日出 清音寺路线 14:30 ~ 15:00 观光车到达清音寺, 0.5 小时 15:00 ~ 15:30 清音寺,一线天观光, 0.5 小时 15:30 ~ 17:30 到达雷洞坪, 2.0小时 17:30 ~ 18:30 入住酒店,雷洞坪购物街, 1.0 小时 18:30 ~ 19:30 晚饭时间, 1.0 小时 19:30 ~ 晚上早点休息, 次日需凌晨 4:30 起床看日出 04:30 ~ 05:30 起床早餐 05:30 ~ 06:30 到达接引殿, 1.0 小时 06:30 ~ 07:30 缆车排队预计 1 小时, 缆车 20 分钟, 下缆车走路 10 分钟到金顶 07:30 ~ 09:00 看日出,观云海 08:30 ~ 09:30 乘坐缆车返回接引殿 09:30 ~ 10:30 返回雷洞坪, 回酒店提行李 10:30 ~ 12:00 观光车到达峨眉山出口 12:00 ~ 开始返程 共计 1.5 天 ...

2021-09-28 · 2 min · 301 words · Cee Yang

Swift Debug Print

自定义 swift 控制台输出 // MARK: - Debug Print - public func print<T>(_ message: T, fileName: String = #file, methodName: String = #function, line: Int = #line) { #if DEBUG let printInfo = """ \n ================================================================================================================================================= 【File Name】: \(fileName.split("/").last ?? "") 【FunctionName】: \(methodName) 【Fuction Line】: \(line) 【Print Time】: \(Date()) 【 Message 】: \(message) ================================================================================================================================================= \n """ print(printInfo, separator: "", terminator: "") #endif }

2019-08-10 · 1 min · 62 words · Cee Yang

UITextView+Placeholder

Swift UITextView 扩展 Placeholder import UIKit fileprivate var kTextViewPlaceholderLabel : Int = 0x2019_00 fileprivate var kTextViewPlaceholder : Int = 0x2019_01 fileprivate var kTextViewPlaceholderColor : Int = 0x2019_02 fileprivate var kTextViewPlaceholderFont : Int = 0x2019_03 fileprivate var kTextViewPlaceholderKeys : Int = 0x2019_04 extension UITextView { /// 占位符 var x_placeholder: String { get { if let placeholder = objc_getAssociatedObject(self, &kTextViewPlaceholder) as? String { return placeholder } else { return "" } } set { objc_setAssociatedObject(self, &kTextViewPlaceholder, newValue, .OBJC_ASSOCIATION_RETAIN) x_placeholderLabel.text = newValue } } /// 占位符颜色 var x_placeholderColor: UIColor { get { if let placeholderColor = objc_getAssociatedObject(self, &kTextViewPlaceholderColor) as? UIColor { return placeholderColor } else { return UIColor.subTitleColor } } set { objc_setAssociatedObject(self, &kTextViewPlaceholderColor, newValue, .OBJC_ASSOCIATION_RETAIN) x_placeholderLabel.textColor = newValue } } /// 占位符字体 var x_placeholderFont: UIFont { get { if let placeholderFont = objc_getAssociatedObject(self, &kTextViewPlaceholderColor) as? UIFont { return placeholderFont } else { return UIFont.systemFont(ofSize: 12) } } set { objc_setAssociatedObject(self, &kTextViewPlaceholderColor, newValue, .OBJC_ASSOCIATION_RETAIN) x_placeholderLabel.font = newValue } } /// 占位符 标签 @IBInspectable var x_placeholderLabel: UILabel { get { var _placeholderLabel = UILabel(font: self.font ?? UIFont.systemFont(ofSize: 12), color: .subTitleColor, alignment: .left) if let label = objc_getAssociatedObject(self, &kTextViewPlaceholderLabel) as? UILabel { _placeholderLabel = label } else { objc_setAssociatedObject(self, &kTextViewPlaceholderLabel, _placeholderLabel, .OBJC_ASSOCIATION_RETAIN) } addPlaceholderLabelToSuperView(label: _placeholderLabel) return _placeholderLabel } set { objc_setAssociatedObject(self, &kTextViewPlaceholderLabel, newValue, .OBJC_ASSOCIATION_RETAIN) addPlaceholderLabelToSuperView(label: newValue) } } /// 是否需要添加占位符到父视图 fileprivate var x_placeHolderNeedAddToSuperView: Bool { get { if let isAdded = objc_getAssociatedObject(self, &kTextViewPlaceholderKeys) as? Bool { return isAdded } return true } set { objc_setAssociatedObject(self, &kTextViewPlaceholderKeys, newValue, .OBJC_ASSOCIATION_RETAIN) } } /// 添加占位符到父视图 /// /// - Parameter label: 占位符 标签 fileprivate func addPlaceholderLabelToSuperView(label: UILabel) { guard x_placeHolderNeedAddToSuperView else { return } x_placeHolderNeedAddToSuperView = false NotificationCenter.default.addObserver(self, selector: #selector(x_textChange(noti:)), name: UITextView.textDidChangeNotification, object: nil) addSubview(label) label.snp.makeConstraints { (make) in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 7, left: 2, bottom: 0, right: 0)) } } /// 编辑事件 @objc fileprivate func x_textChange(noti: NSNotification) { let isEmpty = text.isEmpty print("text:\(String(describing: text))\nisEmpty:\(isEmpty)") x_placeholderLabel.text = isEmpty ? x_placeholder : "" x_placeholderLabel.isHidden = !isEmpty } }

2019-08-08 · 2 min · 316 words · Cee Yang

iOS UITabBarController 嵌套 UINavigationController

iOS UITabBarController 嵌套 UINavigationController 本文处理两个问题: 1. 自定义导航按钮后, 滑动返回手势失效问题 2. TabBar 嵌套 NavigationBar 的 TabBar 显示隐藏错乱等问题 主要思路 使用 UITabBarController 作为 Window 的 rootViewController 自定义 UINavigationController 控制器, 全局控制 tabbar 的显示与隐藏 添加自定义控制器为 UINavigationController 的 root 控制器 UITabBarController 添加每个 UINavigationController 控制器 直接上代码 自定义的 UITabBarController : import UIKit class HomeTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() createChildController() } /// 通过自定义方法添加所有子控制器 func createChildController() { addChildVC(childVc: HomeViewController(), title: "首页", image: "IMG_Home", selectedImage: "IMG_Home_Selected") addChildVC(childVc: MoreViewController(), title: "应用", image: "IMG_More", selectedImage: "IMG_More_Selected") addChildVC(childVc: MineViewController(), title: "我的", image: "IMG_Mine", selectedImage: "IMG_Mine_Selected") } /// 自定义添加子控制器 func addChildVC(childVc: UIViewController, title:String, image: String, selectedImage: String) { let nav = HomeNavigationController(rootViewController: childVc) let normalImage = UIImage(named: image) let selectedImage = UIImage(named: selectedImage) nav.navigationItem.title = title nav.tabBarItem = tabbarItem(with: title, normalImage: normalImage!, selectedImage: selectedImage!) addChildViewController(nav) } /// 快捷创建 UITabBarItem func tabbarItem(with title: String, normalImage: UIImage, selectedImage: UIImage) -> UITabBarItem{ let image = normalImage.withRenderingMode(.alwaysOriginal) let _selectedImage = selectedImage.withRenderingMode(.alwaysOriginal) let tabBarItem = UITabBarItem(title: title, image: image, selectedImage: _selectedImage) tabBarItem.setTitleTextAttributes([NSAttributedStringKey.foregroundColor: UIColor.mainColor], for: .selected) tabBarItem.setTitleTextAttributes([NSAttributedStringKey.foregroundColor: UIColor.tabbarNormalColor], for: .normal) return tabBarItem } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } } //让图片和文字在 iPad 下仍然保持上下排列 extension UITabBar { override open var traitCollection: UITraitCollection { if UIDevice.current.userInterfaceIdiom == .pad { return UITraitCollection(horizontalSizeClass: .compact) } return super.traitCollection } } 自定义的 UINavigationController : ...

2018-09-18 · 2 min · 337 words · Cee Yang

5分钟带你看完 WWDC 2018

前言 一年一度的 WWDC(苹果全球开发者大会)于北京时间 6月5日 凌晨1点开幕。废话不多说,来看看这次WWDC 都有哪些亮点吧! iOS 12 和 ARKit 2.0 关键词:官方防沉迷最为致命 iOS 12 iOS 12 相较于 iOS 11 并没有太多UI上的变动,刚更新完 bate 版本的 iOS 12,完全感觉不到这是个新系统。 iOS 12 主要是对安全和性能的优化,iOS 12 在旧设备上的运行速度比 iOS 11更块,程序加载速度快了一倍。(PS:看来苹果并没有放弃旧设备) ARKit 2.0 Apple 与 皮克斯 合作开发了一种用于共享AR内容的新文件格式,新的 AR 格式名为 USDZ。 作为一个含着金苹果出生的新生儿,USDZ 一开始就得到了 Adobe Creative Cloud (包括 Photoshop CC、InDesign CC、Illustrator CC、Dreamweaver CC、Premiere Pro CC)套件的支持。 同时,面向开发者的开发套件 ARKit 则升级到了二代,主要提升了面部跟踪、渲染能力、3D 探测和共享体验等能力。 随后展示了一款名为 Measure 的 App,可使用AR查看物品大小。 最后为了演示新的 AR 能力和效果,苹果请来了乐高的创意总监来捧场。这是一个真实的乐高积木建筑物为基础,最多四个人可以用苹果 AR 应用进行游戏,可以在真实环境中模拟出各种虚拟的形象和建筑。 ...

2018-06-05 · 2 min · 387 words · Cee Yang

Xcode命令行工具管理

安装 xcode-select --install Xcode版本切换 显示当前使用的xocde版本 $ xcode-select --print-path 选择Xcode中的默认版本 $ sudo xcode-select -switch /Applications/Xcode.app

2018-05-05 · 1 min · 14 words · Cee Yang

Swift 4.2 新特性

private 权限扩大 在 Swift 4 中,extension 可以读取 private 变量了。 Swift 3 中,如果将主体函数的变量定义为 private,则其 extension 无法读取此变量,必须将其改为 filePrivate 才可以。 单向区间 单向区间是一个新的类型,主要分两种:确定上限和确定下限的区间。直接用字面量定义大概可以写成 …6和 2… 例如 let intArr = [0, 1, 2, 3, 4] let arr1 = intArr[...3] // [0, 1, 2, 3] let arr2 = intArr[3...] // [3, 4] 字符串改动 String 操作简化了 String 许多要通过 .characters 进行的操作,可以直接用 String 进行操作了。 例如: let greeting = "Hello, 😜!" // No need to drill down to .characters let n = greeting.count let endOfSentence = greeting.index(of: "!")! 新增 Substring 类型 swift 4 为字符串片段新增了一个叫 Substring 的类型。 当你创建一个字符串的片段时,会产生一个 Substring 实例。Substring 与 String 用法相同, 因为子串和原字符串共享内存,所以对子串的操作快速而且高效。 let greeting = "Hi there! It's nice to meet you! 👋" let endOfSentence = greeting.index(of: "!")! // 产生 Substring 实例 let firstSentence = greeting[...endOfSentence] // firstSentence == "Hi there!" // `Substring` 与 `String` 用法相同 let shoutingSentence = firstSentence.uppercased() // shoutingSentence == "HI THERE!" 但是要注意一个 Substring 保留从其生成的完整的 String值。 当您传递一个看似很小的 Substring 时,这可能导致意外的高内存开销。所以使用 Substring时,最好转化为 String. ...

2017-09-11 · 2 min · 321 words · Cee Yang

Git 快速入门教程

Git 使用教程 一、Git 的安装 Windows版本 : 直接前往git官网下载msysgit安装包进行安装即可,安装流程和一般的windows软件的安装流程差不多,没什么区别。 Mac版本: Mac已经自带了git,无需安装。 二、配置Git提交的用户名和邮件 这样做的目的是为了在git的log日志里面可以清楚的区分出每次的提交人是谁,以防日后出了问题能够立马清晰准确的定位出是哪个屌丝挖的坑,操作步骤: 打开 git 控制台: Windows:随便在哪个空白位置右键,然后 git bash,调出git的命令控制台。 Mac:随便打开一个你喜欢的终端即可(Terminal, Iterm2 等)。 然后输入以下命令: $ git config --global user.name "Your Name(用户名)" $ git config --global user.email "[email protected](邮箱)" 这里简单的说明一下,加上--global参数是表示配置的全局范围,针对所有的git项目而言的,当然也可以分别给某一个项目配置,进入到某个项目里面,去掉–-global参数即可。 三、生成 SSHKey Git推荐使用SSH协议传输文件(代码),会生成一个公钥和私钥,公钥配置在服务端,每次可以省去验证,方便快捷。当然使用https协议也是可以的,只不过是每次都需要输入用户名和密码,相对来说比较繁琐。 步骤: 任意空白处右键调出git Bash命令控制台,如果是Mac系统,打开终端就行了,输入以下命令: $ ssh-keygen -t rsa -C "[email protected]" 然后一路回车,使用默认值即可,应该是需要3个回车的,设置密码为空(注意按3个回车的时候别输入其他的,不然设置的密码不为空,你就等着每次提交输入密码吧)。 如果一切顺利的话,可以在用户主目录里找到.ssh目录,里面有 id_rsa 和 id_rsa.pub 两个文件,这两个就是SSH Key的秘钥对,id_rsa 是私钥,不能泄露出去,id_rsa.pub 是公钥,可以放心地告诉任何人。 一般这个 id_rsa.pub 里面的内容是要配到git服务器上面的(由统一的人去管理),然后我们就可以从服务器上面拿代码和提交代码了。Mac系统默认目录为~/.ssh。 四、获取代码与提交代码 首先给大家介绍几个概念,一个是本地 worksapce 工作区,一个是 stash 暂存区,一个是 localRepository 本地库,一个是 remoteRepository 远程库。 从git服务器下载代码到本地 git clone [email protected]:ceeyang/ceeyang.github.io.git 查看本地分支和远程分支,其中-a参数查看远程分支 git branch git branch -a 创建本地分支 git brnach newBranch 切换本地分支 git checkout newBranch 创建本地新分支并切换到该分支 git checkout -b newBranch 基于本地分支创建远程分支 git push origin newBranch 删除分支(注意:删除分支需要先切换到其他的分支,如master分支,再执行删除操作) git checkout master git branch -d dev 创建分支并且切换分支 基于远程分支创建本地分支并且切换到该本地分支,基于远程 dev 分支在本地创建一个 dev 分支,并且切换到 dev 分支。 // 1 git checkout -b newBranch origin/newBranch // 2 git checkout -b newBranch // 3 git checkout newBranch 暂存本地工作区的修改 git add README.MD 通常使用用(".“代表所有文件,注意”.“前面的空格) ...

2017-06-25 · 3 min · 522 words · Cee Yang

岁月静好

若我只是你一红尘过客, 愿我不曾扰你 愿岁月静好。 一个人,一杯咖啡,一台电脑,一下午。 在悠扬的旋律中将自己的心事铺开,往事如似水流年,在眼前一幕幕滑过,或清晰如画,或淡淡如烟。 很是感谢这些年你的陪伴,我将倾尽我所有,为博与你共赴余生。 如果终将无法相濡以沫,那就相忘于江湖吧。 我会记得你,祝岁月静好。

2017-06-25 · 1 min · 8 words · Cee Yang