跳转到内容
彼岸论坛

小天管理

管理员
  • 内容数

    15876
  • 注册日期

  • 最后上线

  • 得奖次数

    1

小天管理 发表的所有内容

  1. 我哥家里是用蓝牙遥控器的夏普电视,因此不支持小爱音箱控制。买了 appletv ,每次看电视,开关机和调音量都必须要用电视本身的蓝牙遥控器,然后再换到 appletv 遥控器,用起来很麻烦。 所以我的问题来了: 这种本身是蓝牙遥控的电视,有办法可以改成红外遥控的吗,然后就支持让小爱音箱控制,也支持让 appletv 遥控器控制电视和 appletv ,无需来回切换。
  2. 正在开发一个 app ,用户在使用 app 访问服务时,需要根据对应的服务从后端获取对应的配置文件( json 格式)。 一共有上千个独立的配置项。 目前是当 app 第一次启动时,会首先通过接口查询配置项清单,然后再依次对各配置项进行请求获取。 这样的问题是,一个 app 就会向后端发起上千个请求。而且可能需要十来分钟甚至更长时间才能把全部配置拉下来。 这样一方面对后端服务器造成压力,另一方面影响用户体验。 如果把配置全部打包在一起的话,大概40-50MB左右。 有些配置项还会更新,这就需要app 在后续的运行过程中对有更新的配置项进行更新。 请问大佬们有什么好的思路?
  3. 有没有类似十年之约的这类站点推荐?
  4. 1, 直播 App 上线非常复杂吗?客户刚刚新注册的公司,能否申请下来相关资质? 2 ,是否有开源产品可以替代的,私有化部署的直播 App ,自己搭建服务器,内部人员使用。 **说明,不涉及违法的事情,就是客户方不希望走公共直播平台。 希望有经验的朋友 给指点一二,谢谢!
  5. 简约版: 公司做海外业务的,现做一款游戏推荐平台,海外 8 个国家上线,30 多万日活; 公司已盈利,资金健康。 需求: 2~5 年经验产品经理,希望有 C 端经验,对 web3 、AI 感兴趣或有实际参与经验的加分; 薪资:15~30k ,具体视经验、匹配度、潜力评估; 有意者联系: V:MTM1MTA3MDk1MzE=( base64 )
  6. 要去欧洲出差几个月,想要自己弄一个 VPS 线路回国,想征求一下可行性、稳定性以及对应的法律风险 有朋友说回国的 VPN 服务不会被 ban ,那是不是可以直接 wirequard 或者最传统的 vpn 方案 如果不行,使用现在主流的协议,用 xui 面板弄一个 vless/shadowsocks 方案是否可行 最后的备选方案是 Trojan 自用,准备使用宁波双向 20m 的 vps 搭建
  7. 2023.3 月 pdd 买的 macbook pro 2021 。 2023.12 月 发现无法充电,在保修内更换了电池和主板。 2024.9 月 又发现无法充电了。 所以,每隔 8-9 个月,电池就废了? 很无奈啊。
  8. 如题,目前正在使用的是一把固定的椅子,就是海绵坐垫和海绵靠背,别的什么都没有,我坐起来也并没有什么不舒服的地方。所以想问一下大家。动辄上千块的人体工学椅究竟好在哪里?相同功能的椅子,PDD 上没有品牌只要 200 块,带上品牌就要 500 块。这其中的差价抛开材质和做工,在坐的体验上究竟能有多少提升?功能更多的椅子 2000 块也是有的,前天下午我坐着 20 块买的较高的马扎打了一下午游戏也并没有觉得有什么不妥,身体也没有产生不舒服的感觉( 30 岁了)。之前去家具城也体验了各种价位的人体工学椅,我的感觉是没有感觉。椅子的本质就是坐,我可以理解材质做工功能的提升进而价格提升,但是以人体工学为宣传噱头、以身体健康来制造焦虑,这样的价格提升除去有钱人怎么样的人会去买单呢?我是穷卫(鬼),轻喷。
  9. 双卡 iPhone 14 Pro Max 的兄弟们,主卡电信,副卡移动,朋友打你卡 2 (移动)的时候会不会偶尔听不清晰 不知道大家会不会有这样,当时当你主卡电信,副卡移动的时候,都是满信号并且信号覆盖不错的情况下,朋友打你卡 2 移动的时候,说你这边的声音怎么听不清晰。 我是 14PM ,系统是首发初始系统 iOS 16.0 从未升级过的 我是卡 1 电信主力,卡 2 移动(保号)专门打家庭短号网的
  10. 各位使用成都、四川移动地区专线的 V 友们,请关注自己专线的网络状况: 由于打压 PCDN ,从 2024.9.1 日下午 16:00 以后,成都地区移动出现了只要有上传流量(流速>10mbps ),会出现到网关的高 ping 、跳包现象。同时上传速度仅仅只有正常速度的 10/1 。 我这里已经有两条专线中招,有一条日均上传很高(作用于网盘备份、同步),而另外一条日均上传不过 5-15G ,一个月下下上行流量不过 137G 。 现在的情况就是:下行可以跑满合同限速,ping 不丢包,只要测速开始测试上传,并且速率>10m ,到第一跳网关将出现高 ping 导致整个网络连接质量极差,停止上传后网络会立即恢复正常。 tips:1 、这些都是固定 IP 的专线,不是家宽; 2 、最开始我还认为只针对高上传用户,结果低上传用户也被一锅端了。 3 、不跑 PCDN 、PT ,不要拿跑 PCDN 被收拾来幸灾乐祸。这个 QOS 策略纯纯傻逼。 大家对照一下检查一下专线的情况把 图片: https://imgur.com/a/xz6kYwV
  11. 最近提了离职,因为工资和工作环境问题,刚才和老板谈工资清算没谈拢… 入职说好的 5%的项目提成,我这边的工作已经做完了,但是现在项目还没有验收,所以貌似公司不打算给我….这样可以吗? 我提 N+1 的时候又说要满一年才有,但是劳动法规定的 6-12 个月是按一年算的吧? 如果谈不拢有必要走劳动仲裁吗? 还是我有问题?
  12. 我的域名是很久以前通过 cf partner 面板接入的 cname 记录。仅接入了子域名 cdn.a.com(源站 b.com),没有接入 a.com 和 www.a.com。接入时的设置是通过 http 续签证书。也就是在根目录下设置 http://cdn.a.com/.well-known/pki-validation/caxxxxx.txt 文件,cf 安排续签证书时校验这个文件验证所有权。 前天突然发现无法通过 https 访问 cdn.a.com。http 访问则没有问题。直接 https 访问源站 b.com 也没有问题。 通过 openssl s_client -connect cdn.a.com:443 -showcerts 检查发现没有证书: CONNECTED(00000003) 140170526717840:error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure:s23_clnt.c:769: no peer certificate available 登录 cf 后台,在 [网站] [ SSL/TLS ] [边缘证书] 页面中,边缘证书列表显示为无证书: 我已检查过: [ SSL/TLS ] 被设置为完整(严格)。 没有开启 [始终使用 HTTPS ] 。确保续签时可通过 http 访问校验文件。 [安全性] [ WAF ] 中已设置规则,放行对校验文件的访问。 外部 DNS 解析服务中 cdn.a.com 通过 cname 指向官方指定的 cdn.a.com.cdn.cloudflare.net。 curl http://cdn.a.com/.well-known/pki-validation/caxxxxx.txt 可以得到预设的值。 我已试过如下方法但都没有效果: cf 后台暂停该域名,一小时后重启。 [ SSL/TLS ] 调整为关闭,一小时后恢复为完整(严格)。 [ SSL/TLS ] 调整为关闭,一小时后恢复为完整。 [边缘证书] 中关闭 [通用 SSL ] ,一小时后启用。 按下图提示,在外部 DNS 解析服务中设置 _acme-challenge.cdn.a.com cname 指向 cdn.a.com.uuid.dcv.cloudflare.com,然后关闭 [ SSL\TLS ] 和 [通用 SSL ] 十几分钟后恢复。
  13. ![1000013040.webp]( https://j.teno.cx/root/2024/09/01/66d47e29cf906.webp) 虽然之前有人发了,但实在太神奇所以再补充下,原理可以看参考资料:,https://www.v2ex.com/t/1014864 方法就是打开 prefix hint 功能, ![1000013041.webp]( https://j.teno.cx/root/2024/09/01/66d47f8d48c53.webp) 输入当前 IP 前缀或者按照 bras 的前缀写全尝试也可以,也就是可以调靓号,也可以全 0 补全,(如图上就是 0 补全)如果报错就是 bras 拒绝了手动的前缀 实测本地移动全 0 最短前缀可以,/48 ~/60 的子网部分 随便改靓号,电信如果改的太短或者太靓号会强制修改几个位,没法全 0 最短前缀,但只要分配给我了就可以一直用 # 此方法修改后重拨号前缀固定,理论上只要 bras 方这段前缀不过期就可以一直用,算静态 ipv6 体验卡 关于如何 win 系统用短 ipv6 , win 系统要把 ipv6 临时地址和 ra 通告给禁用了,适配器里面再改成手动获取 ipv6 地址 实现 12 位超短 ipv6 地址 linux 系统更简单,直接配置静态 ipv6 即可, 安卓只支持 slaac 短不了一点,iOS MacOS 似乎没法静态 ipv6 ,用有状态 dhcpv6 可以分固定 v6 ,(但 ROS 不支持有状态)
  14. 之前 pt 一直是直接使用群晖的 ds ,前一阵子看到 mp 感觉可以整自动化一些。但发现 mp 需要特定网站认证,而我只有 mt ,无法认证。 故求下列网站的一个要求 iyuu/hhclub/audiences/hddolby/zmpt/freefarm/hdfans/wintersakura/leaves/ptba /icc2022/xingtan/ptvicomo/agsvpt/hdkyl/qingwa/discfan/haidan/rousi 保证完成新手任务。 这是我 mt 的数据 上傳量: 4.06 TB 下載量: 3.13 TB 做種/下載時間比率: 19.59 邮箱地址:bGltY2hpaGlAZ21haWwuY29t ( base64 )
  15. 朋友有一台 MacBook Air 2018 ,可能升级 Sonoma14.6.1 时候进度条卡住了一个小时,被强制关机了。 结果,无法正常开机,Apple 进度条走完之后,就被要求输入密码来解锁。然后输入 Mac 密码后又无限循环 logo 进度条输密码… 1 、首先尝试 Mac 自带恢复模式进行升级操作,尴尬的是磁盘空间不足差了 3GB ,没辙。 2 、于是尝试用制作 U 盘启动器的方式来升级,但是选择升级过程中报错「这个“安装 macOS Sonoma”应用程序副本已损坏,不能用来安装 macOS 」,终端设置 date 时间无效。 3 、重置 PRAM/NVRAM 无效。 4 、shift 按住进入安全模式无效。 5 、无时间机器备份 目前是不是除了抹掉磁盘以外没招了?
  16. 1 、如在 PCB 板和原理图中用了( XH2.5-2A 直针 2PIN 针座),要找适配它的母头元件准备加入到清单,怎么找最便捷、准确? 这个自己瞎摸索过,是不是除了历史以来熟悉搭配型号,只能手选,别无它法? 2 、在自动布线的时候,限定某些区域不走线,不产生走线过孔,比较灵巧方式是如何做? 是不是用个什么容易放置、删除的东西“占位”,自动补完线后删除占位内容? 3 、以前看别人做电子制作,在一整块 PCB 板上画出一部分区域(连板?),可以像梳打饼干那样折断这部分区域,成为单独的 PCB 板块,这种划开区域的做法,在立创 EDA 里怎么做? 1.6MM 厚的板子。。。
  17. 如题,坐标广州,开的电信,应该是 500Mb 的带宽,不管是有线连接的台式机+PS5 ,还是我的 wifi 连接的笔记本,网速都很快,下载也从来没问题,但是手机网络就经常出问题,包括但不限于微信、飞书图片半天刷不出来(我的使用体验),微博、小红书的图片和视频也加载特别慢(我对象的使用体验)等。很奇怪啊,有大佬了解吗? 路由器是 RT-AX88U Pro 提前拜谢大佬!
  18. 在甲骨文免费的机器上用 docker 运行着一个 vaultwarden 服务,用来管理自己的所有密码。 因为是免费的 vps ,为了避免机器被忽然回收导致数据丢失,因此需要定期备份 vaultwarden 的数据库。 首先 vaultwarden 使用了 docker compose 部署,部署文件如下: services: vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: always environment: SIGNUPS_ALLOWED: false # Deactivate this with "false" after you have created your account so that no strangers can register volumes: - ./vw-data:/data # the path before the : can be changed ports: - 11001:80 # you can replace the 11001 with your preferred port 因此在 vw-data 下面,保存着所有密码数据,只要备份这个目录,就可以还原。 备份工具使用 restic ,它支持将数据直接备份到远程,下面步骤主要是设置好备份地址,然后定期执行的操作细节: 一: 首先安装 restic sudo apt update sudo apt install restic 二: 初始化 restic 仓库 先设置好环境变量: export RESTIC_REPOSITORY=s3:https://<minio-host>:<minio-port>/<bucket-name>/<subpath> export AWS_ACCESS_KEY_ID=<your-access-key> export AWS_SECRET_ACCESS_KEY=<your-secret-key> export RESTIC_PASSWORD=<your-password> 然后执行: restic init 执行成功后,会在远程 S3 目录 https://<minio-host>:<minio-port>/<bucket-name>/<subpath> 看到一些初始化 meta 数据。 三: 创建备份并清理旧数据的脚本 backup_and_prune.sh ,内容如下: #!/bin/bash # 设置环境变量 export RESTIC_REPOSITORY=s3:https://<minio-host>:<minio-port>/<bucket-name>/<subpath> export AWS_ACCESS_KEY_ID=<your-access-key> export AWS_SECRET_ACCESS_KEY=<your-secret-key> export RESTIC_PASSWORD=<your-password> # 备份目录 BACKUP_SOURCE="/home/ubuntu/vaultwarden/vwdata" # 日志文件 LOG_FILE="/home/ubuntu/back.log" # 执行备份并记录日志 restic backup $BACKUP_SOURCE >> $LOG_FILE 2>&1 # 删除超过 7 天的旧备份并记录日志 restic forget --keep-daily 7 --prune >> $LOG_FILE 2>&1 # 记录快照信息 restic snapshots >> $LOG_FILE 2>&1 请自行将里面的变量和路径替换为实际值。 四: 设置定期执行: 使用 cron 来设置每天定期执行备份任务。 crontab -e 添加以下行来设置每天凌晨 2 点执行备份脚本: 0 2 * * * /home/ubuntu/backup_and_prune.sh 确保你的脚本有执行权限: chmod +x /home/ubuntu/backup_and_prune.sh 这样你的备份脚本就会每天凌晨 2 点执行一次了。 五: 验证备份 验证数据是否正常的最好办法是让 vaultwarden 加载备份的数据,然后在网页查看数据是否是最新的。因此接下来的操作是将远程的备份下载到本地,然后在本地执行的具体操作。 首先设置环境变量 export RESTIC_REPOSITORY=s3:https://<minio-host>:<minio-port>/<bucket-name>/<subpath> export AWS_ACCESS_KEY_ID=<your-access-key> export AWS_SECRET_ACCESS_KEY=<your-secret-key> export RESTIC_PASSWORD=<your-password> 列出所有快照: restic snapshots 恢复某个快照到指定目录: restic restore <snapshot-id> --target ~/Downloads/ 根据本文的例子,最终的文件还原到了 ~/Downloads/vw-data 下面。 本地临时运行一个 docker compose 文件: services: vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: always environment: SIGNUPS_ALLOWED: false # Deactivate this with "false" after you have created your account so that no strangers can register volumes: - ~/Downloads/vw-data:/data # the path before the : can be changed ports: - 11001:80 # you can replace the 11001 with your preferred port docker compose up 启动后,访问 localhost:11001 查看数据正常就表示备份没有问题了。 本文首发于: https://blog.tomyail.com/backup-vaultwarden-data-using-restic/ 转载请注明出处
  19. 坐标成都。今天同事说网站打不开了,看不到文档。 请问是遇到什么变故了吗?临时的还是永久的?
  20. 前言 最近更新了我的系列文章,其中有一部分是关于 JavaScript 语言中“点表示法”的使用。 文章中有 5 个工具函数是纯JavaScript的,我觉得不仅仅是小程序项目用得上,其他前端 JS 项目也应该用得上。 我把文章中的这一部分单独整理出来,给需要写前端代码的朋友参考参考。 你可以直接在 Github 项目 中的前端 utils.js 文件中找到这 5 个工具函数。 下面是文章节选,完整的文章列表可以在 Github 项目 中查看。 点表示法a.b.c 用点表示法给对象赋值:putValue函数 想象一下,如果a是一个对象,你要给a.b.c.d赋值为 1 ,你会这样写? let a // 通过某种方式获得的对象 if (!a.b) { a.b = {} } if (!a.b.c) { a.b.c = {} } a.b.c.d = 1 拜托,大学生才这样写。为此,我们引入了putValue函数: utils.putValue(a, 'b.c.d', 1) putValue函数会自动创建a.b、a.b.c中间对象,它的声明如下: /** * 向对象中按照路径赋值,如果路径上的中间对象不存在,则自动创建。 * @param {Object} obj - 目标对象。 * @param {string} key - 属性路径,支持'a.b.c'形式。 * @param {*} value - 要设置的值。 * @param {Object} [options={}] - 可选参数。 * - {boolean} remove_undefined - 如果为 true 且 value 为 undefined ,则删除该属性。 * @throws {Error} 如果 obj 为 null 或 undefined ,或路径不合法(如中间非对象)则抛出异常。 */ putValue(obj, key, value, {remove_undefined = true} = {}){ // ... } 使用这个函数有两个地方要注意,一个是如果路径上的中间对象不是对象,会抛出异常。例如a.b=2,这里b不是对象,此时会抛出异常。 另一个是如果value是undefined,则会删除该属性。例如下面的代码: utils.putValue(a, 'b.c.d', undefined) console.log(a) // {b: {c: {}}},putValue 会自动创建中间对象,但不会自动删除空对象 但若你真想赋值为undefined,可设置remove_undefined参数为false。 读取对象属性值:pickValue函数 对应的,如果要获取a.b.c.d的值,可以使用pickValue函数: utils.putValue(a, 'b.c.d', 1) const value = utils.pickValue(a, 'b.c.d') console.log(value) // 1 当然你也可以直接使用javascript的原生语法: const value = a.b?.c?.d 这两种写法,当中间路径不存在时,均会返回undefined。 但a.b?.c?.d这种写法是硬编码,而在实际开发中路径可能是动态的。例如用户要修改一个配置项,这个配置可能是user_config.font.size也可能是user_config.page.color.background,如果使用硬编码的方式,可能会写出这样的代码: if (key === 'font.size') { user_config.font.size = value } else if (key === 'page.color.background') { user_config.page.color.background = value } // 更多的 if 语句... 这样写显然不够优雅,看看putValue与pickValue的组合用法。 // 写入用户配置: utils.putValue(user_config, key, value) // 读取用户配置 const value = utils.pickValue(user_config, key) 不管key怎么变,一句话搞定,感觉一下子和大学生拉开差距了是吧? 向数组末尾添加元素:pushValue函数 在实际开发中,常有向数组末尾添加数据的需求。例如记录用户最近的评论,此时你可以使用pushValue函数: const user_data = {} // 用户数据 let comment = {content: '顶'} // 用户的评论 utils.pushValue(user_data, `articles.recent_comments`, comment) console.log(user_data) // {articles: {recent_comments: [{content: '顶'}]}} 在上面代码中,pushValue函数先是自动创建了user_data.articles.recent_comments数组,然后把comment添加到数组末尾。pushValue函数的声明如下: /** * 将值推入对象指定路径的数组中,若路径或数组不存在则自动创建。 * @param {Object} obj - 目标对象。 * @param {string} key - 数组属性的路径,支持'a.b.c'形式。 * @param {*} value - 要推入的值。 * @throws {Error} 如果路径不是数组,则抛出异常。 */ pushValue(obj, key, value){ // ... } 再次调用此函数,数组中就会有两个评论: comment = {content: '再顶'} utils.pushValue(user_data, `articles.recent_comments`, comment) console.log(user_data) // {articles: {recent_comments: [{content: '顶'}, {content: '再顶'}]}} 向对象中添加多个属性:putObj函数 前面我们使用putValue函数向obj对象写入了一个属性值,但如果你要写入很多个(例如 100 个)属性值,你可能会使用for循环: let user_config = {} let new_config_keys // 100 个新的配置项(数组) let new_config_values // 对应的 100 个值(数组) for (let i = 0; i < new_config_keys.length; i++) { utils.putValue(user_config, new_config_keys[i], new_config_values[i]) } 这样写没问题,但在实战中,你拿到的用户配置往往不是数组的形式,而很可能是一个对象,例如: let new_config = { font: { size: 16, 'family.first': 'Arial', 'family.second': 'sans-serif', }, 'page.color.background': '#fff', // ... } // 你对拿到的配置数据又进一步处理 new_config.update_time = new Date() 这种情况下你可以使用putObj函数一次性写入多个属性值: utils.putObj(user_config, new_config) console.log(user_config) /* 输出如下: { font: { size: 16, family: { first: 'Arial', second: 'sans-serif' } }, page: { color: { background: '#fff' } }, update_time: '...', } */ 注意putObj会自动处理上面new_config变量中各种路径的写法。putObj函数的声明如下: /** * 将一个对象的所有属性按路径添加到另一个对象中。 * @param {Object} obj - 目标对象。 * @param {Object} obj_value - 要添加的属性对象,键支持'a.b.c'形式的路径。 * @param {Object} [options={}] - 可选参数。 * - {boolean} remove_undefined - 如果为 true 且 value 为 undefined ,则删除该属性。 * @throws {Error} 如果 obj 为 null 或 undefined ,或路径不合法(如中间非对象)则抛出异常。 * * 注意 * 若 obj_value 中出现重复路径,则后者会覆盖前者。 * 如 obj_value = {a: {b: 1}, 'a.b': 2},则结果为 {a: {b: 2}} */ putObj(obj, obj_value, { remove_undefined = true} = {}) { // ... } 从对象中获取多个属性:pickObj函数 同样的,我们可以一次性读取多个对象的属性值。例如虽然小程序中的用户配置非常复杂,但当前页面仅关注背景颜色、字体大小等少量配置项,你可以这样使用pickObj函数: let user_config // 某个用户的所有配置 // 本页面需要关注的配置 const keys = ['page.color.background', 'font.size', 'font.family'] // 获取当前页面需要的配置 const curr_config = utils.pickObj(user_config, keys) console.log(curr_config) /* 输出如下: { 'page.color.background': '#fff', 'font.size': 16, 'font.family': { first: 'Arial', second: 'sans-serif' } } */ console.log(curr_config.font) // undefined 注意,传给pickObj函数的第二个参数是一个字符串数组,而不是对象。并且,pickObj返回的对象中,属性值不是以curr_config.font.size这样的形式返回,而是返回curr_config['font.size']。 当然,如果你想要curr_config.font.size这样的形式,可用putObj转换一下: let obj_config = utils.putObj({}, curr_config) console.log(obj_config.font.size) // 16 点表示法在微信小程序中实战演示 为什么要设计这几个函数?为什么要支持config.a.b.c与config['a.b.c']两种写法混用?为什么传给putObj的第二个参数是对象,而传给pickObj的第二个参数是字符串数组?为什么pickObj返回的对象属性值不是config.a.b.c这样的形式,而是config['a.b.c']? 因为这样设计符合实战需求,一句话解释就是:“这样好用”。 下面我们通过几个案例来演示这些函数在实战中的应用。 在 js 中设置用户配置 假设用户首次打开小程序,你需要设置用户默认字体大小为 16 ,背景颜色为白色。可以这样写: let user_config = {} utils.putValue(user_config, 'font.size', 16) utils.putValue(user_config, 'page.color.background', '#fff') 使用putValue设置后,你想修改字体大小和背景颜色?可以这样写: user_config.font.size = 18 user_config.page.color.background = '#000' 在 wxml 中实现修改用户配置 你可能会在 wxml 页面中实现多个配置项的修改,并且使用同一个函数来处理。这时你可以这样写: <button bind:tap="changeConfig" data-key="font.size" value="16" > <button bind:tap="changeConfig" data-key="page.color.background" value="#fff"> changeConfig(e){ const { user_config } = this.data const { key, value } = e.currentTarget.dataset // 从 wxml 中获得点表示法的 key 字符串,直接调用 putValue 函数 utils.putValue(user_config, key, value) // 修改背景色时顺便改一下字体颜色(两种写法混用) if (key === 'page.color.background' && value === '#fff') { user_config.page.color.font_color = '#000' } // 记录最近修改时间 user_config.update_time = new Date() } 在 wxml 中使用用户配置 要在页面中使用page.color.background与page.color.font_color的值,实现根据用户配置显示不同的颜色,可以这样写: <view style="background-color: {{color.background}}"> <text style="color: {{color.font_color}}"> Hello, WxMpCloudBooster! </text> </view> onLoad(){ const { user_config } = this.data const color = utils.pickValue(user_config, 'page.color') this.setData({color}) } 你看,我们传递给pickValue的key根据实际需求可长可短。 初始化默认的用户配置 你希望为每个用户设置一个默认的用户配置,并且你想用常规方式写(不使用点表示法)。可以这样: // 默认配置 const DEFAULT_CONFIG = { font: { size: 16, }, page: { color: { background: '#fff', font_color: '#000' } }, // 这里也可以使用点表示法 a.b.c ,但你不想这样写... } App({ initConfig(){ let { user_config } = this.data utils.putObj(user_config, DEFAULT_CONFIG) // user_config 的其他值会被保留 // 保存用户配置... } }) 注意,上面代码中user_config可能会有其他没有出现在DEFAULT_CONFIG中的配置项,这些配置项会被保留。 记录用户最近发表的内容 假如你已经实现了“记录用户最近发布的评论”功能,代码如下: <button bind:tap="append" data-key="articles.recent_comments" data-prop="comment" > 当用户点击这个按钮时,假设this.data中已经有一个comment对象,你可以这样添加评论: append(e){ const { user_data } = this.data const { key, prop } = e.currentTarget.dataset const value = this.data[prop] // prop === "comment" utils.pushValue(user_data, key, value) // 注意这里用的是 push } 上面这个append函数会把this.data.comment对象添加到user_data.articles.recent_comments数组的末尾。 然后,此时你希望再增加一个按钮,可以把最近的点赞数据this.data.like添加到user_data.articles.recent_likes数组的末尾,那么只需一句: <button bind:tap="append" data-key="articles.recent_likes" data-prop="like" > 完成了,你不需要修改append函数,只需要给data-key和data-prop属性设置不同的值即可。 可见,点表示法很大的目的是为了在wxml中可以方便地指定路径,并在js中方便地处理这些路径。 在页面中修改多个配置项 假设你有一个修改用户配置项的页面,wxml代码如下: <!-- 注意这里有一个 for 循环 --> <view wx:for="{{configs}}"> 配置名称:{{item.title}} 当前值:{{item.value}} 输入新值:<input type="text" /> 点击修改:<button bind:tap="changeConfig"/> </view> 上面代码使用了for循环,configs变量中有多少个值,就会显示多少个配置项。 为了实现在用户打开页面时显示的是用户的当前值(而不是默认值),你还需要从user_config中读取当前用户的配置值。 代码样例如下: // 代码中写死了需要修改的配置项以及默认值 configs = [ {title: '字体大小', key: 'font.size', value: 16}, {title: '背景颜色', key: 'page.color.background', value: '#fff'}, {title: '字体颜色', key: 'page.color.font_color', value: '#000'}, ] // 读取当前用户的配置值 const uc_obj = utils.pickObj(user_config, configs.map(item => item.key)) // 注意,这里的 uc_obj 是 uc_obj['font.size'] 这样的形式,而不是 uc_obj.font.size // 用户当前值覆盖默认值 configs.forEach(item => { item.value = uc_obj[item.key] }) this.setData({configs}) // 传给 wxml 页面显示 这样你就实现了修改多个配置项的页面,用户打开页面时显示的是用户当前的配置值。 提问:假设我们坚决不使用点表示法,且要实现上面这些功能,你要如何设计才能如此简单、高效? 让你的函数也支持点表示法 好了,目前我们花了不少篇幅介绍点表示法,这是因为后面我们会介绍更多的工具函数,而这些工具函数都支持点表示法的调用方式。 当你编写自己的工具函数时,你可以调用putValue、pickValue、pushValue、putObj、pickObj这 5 个函数,轻松地让你的工具函数也支持点表示法。如果你不知道如何实现,可以参考utils.js中其他函数的代码。 (文章节选完,如果你感兴趣的话可以看看 Github 项目)
  21. 有注册成功电商平台 faire 吗 或者有美国身份,对电商感兴趣 有赚钱的机会 欢迎私聊
  22. 不同版本的 gradle 指令改来改去,就算这是两年一改也很是折腾,另外,为什么包管理器一定要绑定 jdk 版本呢 python 同样一个包,兼容 3.8 而与 3.12 冲突? semVer 说好的小版本兼容呢?另外,项目 a 依赖的 b 跟 c 的上层依赖互相冲突,只好分两个 project 搞
  23. Spring RestTemplate 拦截器修改请求体导致的诡异问题 最近在工作中发现了 Spring 的一个"特性"(也许可以叫 Bug ?),反正我已经给 Spring 提了 PR ,等着看能不能合进去。 问题背景 最近在调用第三方 API 时,遇到了一个有意思的场景。整个调用流程大概是这样的: 先调用 /login 接口,发送 username 和 password ,对方服务返回一个 JWT 。 之后的每个请求接口都是标准格式,需要把 JWT 和请求参数放到一个 JSON 中,类似这样: { "token": "JWT-TOKENxxxxxx", "data": { "key1": "value1", "key2": "value2" } } 发送请求,然后拿到响应报文。 解决方案 为了避免在每个接口都重复封装 token ,我想到了用 org.springframework.http.client.ClientHttpRequestInterceptor 来拦截请求,统一修改请求体。 代码大概长这样: this.restTemplate = new RestTemplateBuilder() .requestFactory(() -> new ReactorNettyClientRequestFactory()) .interceptors((request, body, execution) -> { byte[] newBody = addToken(body); // 调用登陆获取 token ,修改入参 body ,添加 token return execution.execute(request, newBody); }) .build(); 诡异的问题 修改完成后,进入测试阶段,奇怪的事情就发生了:token 能正确获取,body 也修改成功了,但对方的接口一直报 400 ,Invalid JSON 。更奇葩的是,我把 newBody 整个复制出来,用独立的 Main 代码发送请求,居然一次就成功了。 深入源码 不服气的我只能往源码里找原因。从RestTemplate一路 Debug 到org.springframework.http.client.InterceptingClientHttpRequest.InterceptingRequestExecution#execute,发现了这么一段代码: @Override public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException { if (this.iterator.hasNext()) { //这里是在执行 interceptor 链,我的登陆和修改 body 接口就在这里执行 ClientHttpRequestInterceptor nextInterceptor = this.iterator.next(); return nextInterceptor.intercept(request, body, this); } else { // 上面的 interceptor 链执行完后,下面就是真实执行发送请求逻辑 HttpMethod method = request.getMethod(); ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method); request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value)); if (body.length > 0) { if (delegate instanceof StreamingHttpOutputMessage streamingOutputMessage) { streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { @Override public void writeTo(OutputStream outputStream) throws IOException { StreamUtils.copy(body, outputStream); } @Override public boolean repeatable() { return true; } }); } else { StreamUtils.copy(body, delegate.getBody()); } } return delegate.execute(); } } 在 Debug 到request.getHeaders().forEach这里时,我突然发现 request 里的Content-Length居然和body.length(被修改后的请求体)不一样。 问题根源 继续往上追溯,在org.springframework.http.client.AbstractBufferingClientHttpRequest中找到了这段代码: @Override protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException { byte[] bytes = this.bufferedOutput.toByteArrayUnsafe(); if (headers.getContentLength() < 0) { headers.setContentLength(bytes.length); } ClientHttpResponse result = executeInternal(headers, bytes); this.bufferedOutput.reset(); return result; } 原来Content-Length在执行拦截器之前就已经被设置了。但我们在拦截器里修改了body,导致对方接收到的 JSON 格式总是不对,因为Content-Length和实际的请求体长度不匹配。 解决问题 这时候为了先解决问题,就先在interceptor中重新赋值了Content-Length this.restTemplate = new RestTemplateBuilder() .requestFactory(() -> new ReactorNettyClientRequestFactory()) .interceptors((request, body, execution) -> { byte[] newBody = addToken(body); // 调用登陆获取 token ,修改入参 body ,添加 token request.getHeaders().setContentLength(body.length); // 重新设置 Content-Length return execution.execute(request, newBody); }) .build(); 测试后,问题解决了。 反思和改进 问题虽然解决了,但我琢磨了一下,虽然是我在拦截器中修改了 body ,但这个地方 Spring 应该还是有责任把错误的Content-Length修正的。 第一,Spring 的文档中没有明确写这里应该由谁来负责,是个灰色地带。 第二,我们用RestTemplate谁会自己设置Content-Length啊,不都是框架设置的吗,所以这里不也应该由框架来负责嘛。 思考完,周末找了个时间给 Spring 提了个 PR ,有兴趣的同学可以到这里看看。Update Content-Length when body changed by Interceptor 有一说一,虽然不是第一次提 PR ,但是还是感觉挺爽的,记录一下。 写的挺乱的,技术一般,大佬轻喷。
×
×
  • 创建新的...