跳转到内容
彼岸论坛

小天管理

管理员
  • 内容数

    15942
  • 注册日期

  • 最后上线

  • 得奖次数

    1

小天管理 发表的所有内容

  1. 各位使用成都、四川移动地区专线的 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
  2. 最近提了离职,因为工资和工作环境问题,刚才和老板谈工资清算没谈拢… 入职说好的 5%的项目提成,我这边的工作已经做完了,但是现在项目还没有验收,所以貌似公司不打算给我….这样可以吗? 我提 N+1 的时候又说要满一年才有,但是劳动法规定的 6-12 个月是按一年算的吧? 如果谈不拢有必要走劳动仲裁吗? 还是我有问题?
  3. 我的域名是很久以前通过 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 ] 十几分钟后恢复。
  4. ![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 不支持有状态)
  5. 之前 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 )
  6. 朋友有一台 MacBook Air 2018 ,可能升级 Sonoma14.6.1 时候进度条卡住了一个小时,被强制关机了。 结果,无法正常开机,Apple 进度条走完之后,就被要求输入密码来解锁。然后输入 Mac 密码后又无限循环 logo 进度条输密码… 1 、首先尝试 Mac 自带恢复模式进行升级操作,尴尬的是磁盘空间不足差了 3GB ,没辙。 2 、于是尝试用制作 U 盘启动器的方式来升级,但是选择升级过程中报错「这个“安装 macOS Sonoma”应用程序副本已损坏,不能用来安装 macOS 」,终端设置 date 时间无效。 3 、重置 PRAM/NVRAM 无效。 4 、shift 按住进入安全模式无效。 5 、无时间机器备份 目前是不是除了抹掉磁盘以外没招了?
  7. 1 、如在 PCB 板和原理图中用了( XH2.5-2A 直针 2PIN 针座),要找适配它的母头元件准备加入到清单,怎么找最便捷、准确? 这个自己瞎摸索过,是不是除了历史以来熟悉搭配型号,只能手选,别无它法? 2 、在自动布线的时候,限定某些区域不走线,不产生走线过孔,比较灵巧方式是如何做? 是不是用个什么容易放置、删除的东西“占位”,自动补完线后删除占位内容? 3 、以前看别人做电子制作,在一整块 PCB 板上画出一部分区域(连板?),可以像梳打饼干那样折断这部分区域,成为单独的 PCB 板块,这种划开区域的做法,在立创 EDA 里怎么做? 1.6MM 厚的板子。。。
  8. 如题,坐标广州,开的电信,应该是 500Mb 的带宽,不管是有线连接的台式机+PS5 ,还是我的 wifi 连接的笔记本,网速都很快,下载也从来没问题,但是手机网络就经常出问题,包括但不限于微信、飞书图片半天刷不出来(我的使用体验),微博、小红书的图片和视频也加载特别慢(我对象的使用体验)等。很奇怪啊,有大佬了解吗? 路由器是 RT-AX88U Pro 提前拜谢大佬!
  9. 在甲骨文免费的机器上用 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/ 转载请注明出处
  10. 坐标成都。今天同事说网站打不开了,看不到文档。 请问是遇到什么变故了吗?临时的还是永久的?
  11. 前言 最近更新了我的系列文章,其中有一部分是关于 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 项目)
  12. 有注册成功电商平台 faire 吗 或者有美国身份,对电商感兴趣 有赚钱的机会 欢迎私聊
  13. 不同版本的 gradle 指令改来改去,就算这是两年一改也很是折腾,另外,为什么包管理器一定要绑定 jdk 版本呢 python 同样一个包,兼容 3.8 而与 3.12 冲突? semVer 说好的小版本兼容呢?另外,项目 a 依赖的 b 跟 c 的上层依赖互相冲突,只好分两个 project 搞
  14. 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 ,但是还是感觉挺爽的,记录一下。 写的挺乱的,技术一般,大佬轻喷。
  15. 本人不太熟悉运维和安全方面的东西,然后现在买了一台 2c2g 的阿里云的 ecs 想要在这台服务器上部署自己写的一些页面然后还有一些对外的 node 服务 但是又考虑到服务器的安全性,还有部署时候的一系列繁琐的操作,就像找个工具统一弄一下 在网上看大家对 1Panel 和 雷池 的评价比较高 不过看了一下对硬件的要求,我的服务器如果同时安装两个,可能内存就撑满了(现在服务器用 docker 装了一个 1panel 和 mysql 就已经占一半了) 现在想问一下各位大佬,装哪一款比较好,或者说这两个根本就是不同纬度的东西,没有可比性 我目前能想到的需求主要是集中在下面两点: https 证书的更新啥的 怕服务器被攻击
  16. 打电话对方听不清,要开免提,天才吧说底部麦克风损坏(当时那 sb 还说的是扬声器),只能自己掏钱整机更换,更换之后保修期只有 90 天 私人维修店说 120 换个“送话器”就能搞定,球问各位靠谱吗 顺便吐槽下 apple store 服务够 low 的,那态度拽的跟你欠了它八百万似的
  17. #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> int g=100; int main() { static int x=666; pid_t pid = fork(); if (pid==-1){ printf("创建进程失败\r\n"); } if (pid==0){ printf("pid=%d g=%d,x=%d,g=%p,x=%p\r\n",pid,g,x,&g,&x); g=111; x=222; printf("我子进程 pid=%d,g=%d,x=%d,g=%p,x=%p\r\n",pid,g,x,&g,&x); }else{ printf("pid=%d,g=%d,x=%d,g=%p,x=%p\r\n",pid,g,x,&g,&x); g=666; x=777; printf("我是父进程 pid=%d,g=%d,x=%d,g=%p,x=%p\r\n",pid,g,x,&g,&x); } return 0; } 输出 pid=4502,g=100,x=666,g=0x601044,x=0x601048 我是父进程 pid=4502,g=666,x=777,g=0x601044,x=0x601048 pid=0 g=100,x=666,g=0x601044,x=0x601048 我子进程 pid=0,g=111,x=222,g=0x601044,x=0x601048 我的问题是: 子进程修改变量后,会重新开启一块新内存,再我重新修改变量值后,为什么在打印变量的地址还是相同的?
  18. 多少刀我忘了反正是 3 年付 0.5t 盘的初期活动鸡, 愿意出的老哥留个 tg? 适当溢价可
  19. 很久没更新翻译软件了,前段时间偶然知道 deepseek ,试用了下,添加到沉浸式翻译,翻译结果非常准确,读起来通顺,以前用过百度,通义千问,字节,腾讯,的 API 感觉都不是太完美,微软,Google 也差点,,,以前一直用的微软,,腾讯偶尔,,其他翻译都差点意思 还没试用过 openai 的翻译,,主要 api 并发问题不好解决,开翻译会员有太贵,, 请问网上那些卖中转 api 的,兼容 openai ,key 的是用 chat2api 项目开发的吗??他们那种怎么那么多账号,撸来的做负载?,,那种中转 api 根 CNY 一比一,不知道放到翻译 api 效果如何,,,
  20. 在阅读 ptrace 相关代码时,看到为了修改 openat 系统调用的 path 参数,在小于 sp 指针(或者叫高于栈顶位置)保存了新的 path 路径,然后将传递参数的寄存器指向了这个新的 path ,这样做为什么是没有问题的?一般文档上说,sp 指向栈顶,那么如果变量使用了没有被 sp 涵盖的内存空间(字符数组指向了小于 sp 指针的位置),这不会报错吗?
  21. 周围同事认为事假会“扣薪”,所以不到万不得已不会轻易请事假。但在本人的认知里,本来人就没去上班,没有当天的薪资很正常,故即使年假用光了也会经常以小时为单位请事假。(被调侃了下“没有生活压力~”) 关于这点认知的差异我稍感兴趣。可能是我一直以来都是按时薪去计算薪资,导致观念里认为自己的工作是计时的;但其他人或许不这么认为,他们大概更倾向于固定薪资(?),所以才会有因事假失去了当天的薪资属于被“扣薪”的观念。 综上,发帖调查统计下其他人的观念。
  22. 原脚本没有对剪贴板的内容进行 urlencode 处理,稍作修改了一下 https://gist.github.com/codexss/03ca2290a89d8becf6c39286b0af537c
  23. 今天想安装个新 Docker ,发现怎么都拉取不到镜像。然后想起来 Dockerhub 被墙了... 于是我把 NAS 的网关和 DNS 地址指向了旁路由网关,旁路由开了梯子。 但是还是不行,折腾了一圈后我把 NAS 的 ipv6 关了,然后就好了。 NAS 是威联通,旁路由是 openwrt+passwall 想请教一下各位,应该怎么设置调整才能 ipv6 和科学共存。因为需要 ipv6 访问 NAS
×
×
  • 创建新的...