<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>张晓刚</title><description>一个技术爱好者。</description><link>https://www.jason-z.com/</link><item><title>程序员日记20250828</title><link>https://www.jason-z.com/posts/programmers-notes-20250828/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20250828/</guid><pubDate>Thu, 28 Aug 2025 09:09:33 GMT</pubDate><content:encoded>



## 微信客服

### App 内接入微信客服提示”deeplink customerservice no permission“

解决方案： 需要登录到**微信客服**的后台，然后关联**微信开放平台**的应用ID



</content:encoded></item><item><title>程序员日记20250808</title><link>https://www.jason-z.com/posts/programmers-notes-20250808/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20250808/</guid><pubDate>Thu, 07 Aug 2025 17:00:21 GMT</pubDate><content:encoded>




## Jenkins



### Jenkins流水线里启动后台进程被清理

默认在流水线里启动后台进程会在Jenkins进行清理的时候被杀死，例如

```
nohup java -jar app.jar &gt; nohup.out &amp;
```

参考官网的[wiki](https://wiki.jenkins.io/display/JENKINS/ProcessTreeKiller)，需要设置环境变量来达到效果

```
JENKINS_NODE_COOKIE=dontKillMe nohup java -jar app.jar &gt; nohup.out &amp;
```





## UTS



### 原生数组类型的写法



例如原生里是这么写

```java
    void onServiceAreaUpdate(AMapServiceAreaInfo[] var1);
```



在UTS里不能这么写

```typescript
  void onServiceAreaUpdate(var1:AMapServiceAreaInfo[]);
```



否则编译成kotlin之后就会变成

```kotlin
fun onServiceAreaUpdate(serviceAreaInfos: UTSArray&lt;AMapServiceAreaInfo&gt;)
```



正确的写法

```
onServiceAreaUpdate(serviceAreaInfos: kotlin.Array&lt;AMapServiceAreaInfo&gt;)
```

</content:encoded></item><item><title>程序员日记20250512</title><link>https://www.jason-z.com/posts/programmers-notes-20250512/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20250512/</guid><pubDate>Mon, 12 May 2025 05:21:51 GMT</pubDate><content:encoded>



## 宝塔



### 未安装phpMyAdmin,请到【软件商店】页面安装!



**解决方法：**

先执行

```shell
echo &quot;check_certificate = off&quot; &gt;&gt; /etc/wgetrc
```

然后重新安装



## IOS

### 无法验证app需要互联网连接以验证是否信任开发者



客户遇到的额一个问题， 用网上的方法都试了，还是不行，最后验证了是联通的网络，导致无法连接到苹果服务器导致的。



## UNIAPP



### createUniXSwiftCompilerOnce is not a function

确保`package.json`里的的`@dcloudio/uni-uts-v1` 与其他 `@dcloud/xxx` 版本一直

```json
&quot;@dcloudio/uni-automator&quot;: &quot;3.0.0-4060420250429001&quot;,
&quot;@dcloudio/uni-cli-shared&quot;: &quot;3.0.0-4060420250429001&quot;,
&quot;@dcloudio/uni-stacktracey&quot;: &quot;3.0.0-4060420250429001&quot;,
&quot;@dcloudio/uni-uts-v1&quot;: &quot;3.0.0-4060420250429001&quot;,
&quot;@dcloudio/vite-plugin-uni&quot;: &quot;3.0.0-4060420250429001&quot;,
```

</content:encoded></item><item><title>程序员日记20250419</title><link>https://www.jason-z.com/posts/programmers-notes-20250419/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20250419/</guid><pubDate>Sat, 19 Apr 2025 03:22:55 GMT</pubDate><content:encoded>



## React



 **MonacoEditor编辑多实例问题**

在开发工具匠《[代码对比](https://toolkk.com/tools/code-diff)》工具的时候，需要在一个页面渲染左右两个编辑器，原代码如下

```react
 const handleCodeChange = useCallback((newValue: string, side: &apos;left&apos; | &apos;right&apos;) =&gt; {
      console.log(`Code change from ${side}:`, newValue);

      if (side === &apos;left&apos;) {
          setLeftCode(newValue || &apos;&apos;);
      } else {
          setRightCode(newValue || &apos;&apos;);
      }
  }, []);

&lt;div className=&quot;flex gap-4&quot;&gt;
    &lt;div className=&quot;w-1/2&quot;&gt;
        &lt;MonacoEditor
            width=&quot;100%&quot;
            height=&quot;400&quot;
            language={language}
            value={leftCode}
            options={options}
            onChange={(newValue) =&gt; handleCodeChange(newValue, &apos;left&apos;)}
        /&gt;
    &lt;/div&gt;
    &lt;div className=&quot;w-1/2&quot;&gt;
        &lt;MonacoEditor
            width=&quot;100%&quot;
            height=&quot;400&quot;
            language={language}
            value={rightCode}
            options={options}  
            onChange={(newValue) =&gt; handleCodeChange(newValue, &apos;right&apos;)}
        /&gt;
    &lt;/div&gt;
&lt;/div&gt;
```

这个时候会出现一个现象，就是即使我们在左编辑器内输入内容，实际上回调的还是右编辑器的方法，也就是hanldCodeChange里一直打印的是right

通过尝试给编辑器加`key`或者`ref` 都没有解决，最终通过查阅，发现根本原因是因为多实例的情况下`monaco`实际上是复用的`model`，所以解决办法就是，将是将编辑器绑定到不同的`model`下,具体代码如下

```react
 const handleEditorDidMount = useCallback((editor, monaco, side) =&gt; {
    if (side === &apos;left&apos;) {
        leftEditorRef.current = editor;
        if (!leftModelRef.current) {
            leftModelRef.current = monaco.editor.createModel(
                leftCode,
                language,
                monaco.Uri.parse(&apos;inmemory://model/left&apos;)
            );
        }
        editor.setModel(leftModelRef.current);
        editor.onDidChangeModelContent(() =&gt; {
            setLeftCode(editor.getValue())
          })
    } else {
        rightEditorRef.current = editor;
        if (!rightModelRef.current) {
            rightModelRef.current = monaco.editor.createModel(
                rightCode,
                language,
                monaco.Uri.parse(&apos;inmemory://model/right&apos;)
            );
        }
        editor.setModel(rightModelRef.current);
        editor.onDidChangeModelContent(() =&gt; {
            setRightCode(editor.getValue())
          })
    }
}, [language, leftCode, rightCode]);

&lt;div className=&quot;flex gap-4&quot;&gt;
    &lt;div className=&quot;w-1/2&quot;&gt;
        &lt;MonacoEditor
            width=&quot;100%&quot;
            height=&quot;400&quot;
            language={language}
            value={leftCode}
            options={options}
            editorDidMount={(editor, monaco) =&gt; handleEditorDidMount(editor, monaco, &apos;left&apos;)}
        /&gt;
    &lt;/div&gt;
    &lt;div className=&quot;w-1/2&quot;&gt;
        &lt;MonacoEditor
            width=&quot;100%&quot;
            height=&quot;400&quot;
            language={language}
            value={rightCode}
            options={options}
            editorDidMount={(editor, monaco) =&gt; handleEditorDidMount(editor, monaco, &apos;right&apos;)}
        /&gt;
    &lt;/div&gt;
&lt;/div&gt;
```



</content:encoded></item><item><title>程序员日记20250402</title><link>https://www.jason-z.com/posts/programmers-notes-20250402/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20250402/</guid><pubDate>Tue, 01 Apr 2025 17:27:38 GMT</pubDate><content:encoded>



## React-Native



### react-android-xxx.aar 下载过慢



手动下载，然后进入到目录`~/.gradle/caches/modules-2/files-2.1/com.facebook.react/react-android/`对应版本的目录下 

找到其中一个含有`pom`文件的文件夹，将下载好的aar复制进去 然后重新运行`yarn android`命令即可。





## 阿里云短信



### 报备的问题

联系了阿里云，短信一直发送失败，说是报备的问题，关键我都报备了半个月了，照他们客服的回复是因为发送量的太少了。。</content:encoded></item><item><title>程序员日记20250331</title><link>https://www.jason-z.com/posts/programmers-notes-20250331/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20250331/</guid><pubDate>Mon, 31 Mar 2025 00:29:40 GMT</pubDate><content:encoded>



## React Native



###   Expo-camera 编译错误

&gt;  Task :expo-camera:compileDebugKotlin FAILED

解决办法：

升级expo的版本

```
&quot;expo&quot;: &quot;^50.0.11&quot;,
&quot;expo-camera&quot;: &quot;~14.1.3&quot;,
```

</content:encoded></item><item><title>程序员日记20250326</title><link>https://www.jason-z.com/posts/programmers-notes-20250326/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20250326/</guid><pubDate>Tue, 25 Mar 2025 20:13:11 GMT</pubDate><content:encoded>



## DeepSeek

* `completions`(补全)API应用

  使用了补全API对工具匠的描述，内容以及标签进行了自动化完善，虽然生成的内容过于刻板，但是总觉得比让员工去弄靠谱些，完善了200个工具只用了不到1块钱和20分钟。



## React Native


- **Boost 安装失败**

找到`/node_modules/react-native/third-party-podspecs/boost.podspec`，替换

```
spec.source = { :http =&gt; &apos;https://boostorg.jfrog.io/artifactory/main/release/1.83.0/source/boost_1_83_0.tar.bz2&apos;,
:sha256 =&gt; &apos;6478edfe2f3305127cffe8caf73ea0176c53769f4bf1585be237eb30798c3b8e&apos; }
```

为

```
  spec.source = { :http =&gt; &apos;https://archives.boost.io/release/1.83.0/source/boost_1_83_0.tar.bz2&apos;,
  :sha256 =&gt; &apos;6478edfe2f3305127cffe8caf73ea0176c53769f4bf1585be237eb30798c3b8e&apos; }
```

或者

```
  spec.source = { :http =&gt; &apos;https://sourceforge.net/projects/boost/files/boost/1.83.0/boost_1_83_0.tar.bz2&apos;,
  :sha256 =&gt; &apos;75b1f569134401d178ad2aaf97a2993898dd7ee3&apos; }
```

具体根据自己的网络情况来。



* SASS插件 [react-native-sass-transformer](react-native-sass-transformer)

  可以在`react native`里使用`scss样式，当然只能转换react native 支持的样式。

  安装

  ```shell
  yarn add --dev react-native-sass-transformer sass
  ```

  配置 `metro.config.js`

  ```
  const { getDefaultConfig, mergeConfig } = require(&quot;@react-native/metro-config&quot;);
  
  const defaultConfig = getDefaultConfig(__dirname);
  const { assetExts, sourceExts } = defaultConfig.resolver;
  
  /**
   * Metro configuration
   * https://reactnative.dev/docs/metro
   *
   * @type {import(&apos;metro-config&apos;).MetroConfig}
   */
  const config = {
    transformer: {
      babelTransformerPath: require.resolve(&quot;react-native-sass-transformer&quot;)
    },
    resolver: {
      sourceExts: [...sourceExts, &quot;scss&quot;, &quot;sass&quot;]
    }
  };
  
  module.exports = mergeConfig(defaultConfig, config);
  ```

  



## Taro

* **别名配置**

`config/index.ts` 配置

```typescript
module.exports = {
  // ...
  alias: {
    &apos;@/components&apos;: path.resolve(__dirname, &apos;..&apos;, &apos;src/components&apos;),
    &apos;@/utils&apos;: path.resolve(__dirname, &apos;..&apos;, &apos;src/utils&apos;),
  },
}
```

`tsconfig.json`配置

```typescript
{
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;.&quot;,
    &quot;paths&quot;: {
      &quot;@/components/*&quot;: [&quot;./src/components/*&quot;],
      &quot;@/utils/*&quot;: [&quot;./src/utils/*&quot;],
    }
  }
}
```

使用

```typescript
import A from &apos;@/components/A&apos;
import Utils from &apos;@/utils&apos;
```





## Uniapp

- **第三方依赖Swift兼容问题**

  云打包一个framework提示兼容性报错

  &gt; error: failed to build module &apos;RingsSDK&apos;; this SDK is not supported by the compiler (the SDK is built with &apos;Apple Swift version 6.0 effective-5.10 (swiftlang-6.0.0.9.10 clang-1600.0.26.2)&apos;, while this compiler is &apos;Apple Swift version 6.0.2 effective-5.10 (swiftlang-6.0.2.1.2 clang-1600.0.26.4)&apos;). Please select a toolchain which matches the SDK. 

​     云打包的Xcode版本目前是16.1 如果高于此版本编译的库 就会报错，但是目前好像低版本也会报错，难道必须相同吗？



## BEM

* 熟悉的陌生人

  实际用了很多，但是第一次才知道这种写法叫做BEM。实际就是一种规范和最佳实践。

  &gt; Block: `nav`
  &gt;
  &gt; Element: `nav__item`
  &gt;
  &gt; Modifier: `nav__item--active`



## Android

* **kotlin compiler embeddable** 下载巨慢的问题

  配置阿里云和腾讯云的仓库貌似不能解决，只能手动下载之后放到下面目录下去

  ```
  ~\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlin\kotlin-compiler-embeddable\版本
  ```

  

</content:encoded></item><item><title>程序员日记20250117</title><link>https://www.jason-z.com/posts/programmers-notes-20250117/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20250117/</guid><pubDate>Thu, 16 Jan 2025 23:26:37 GMT</pubDate><content:encoded>

## Taro

* 使用`NuxUI`的时候部分组件在小程序上可能无法显示;
* `wx.saveFile`即将废弃，需要使用`[FileSystemManager]`文件对象来操作文件;
* 小程序预览的报错`非法的文件，错误信息：invalid file:`,可以在项目配置里 **&quot;将JS编译成ES5&quot;**;



## Nextjs

* `missing-suspense-with-csr-bailout` 错误，官网已经给了解决方案，简单来说就是如果使用`useSearchParam`就应该把客户端组件放置在`&lt;Suspense&gt;` 组件内，否则将会让整个页面全部变成客户端组件



## Yarn

* Yarn命令提示 Error: certificate has expired错误，可进行如下配置

  ```shell
  yarn config set &quot;strict-ssl&quot; false -g
  ```

  

## MySQL

* 利用`UUID`函数生成随机的UID命令

  ```sql
  UPDATE users
  SET uid = LPAD(ABS(CONV(SUBSTR(REPLACE(UUID(), &apos;-&apos;, &apos;&apos;), 1, 8), 16, 10)), 8, &apos;0&apos;)
  WHERE uid IS NULL;	
  ```

  
</content:encoded></item><item><title>程序员日记20241224</title><link>https://www.jason-z.com/posts/programmers-notes-20241224/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241224/</guid><pubDate>Tue, 24 Dec 2024 13:36:17 GMT</pubDate><content:encoded>



## React Native

* RN项目好像修改根目录的`build.gradle`的仓库地址，好像对插件工程的仓库也没有生效，另外也尝试了在`gradle.properties`增加了代理，好像也没有用

  ```glsl
  org.gradle.jvmargs=-DsocksProxyHost\=127.0.0.1 -DsocksProxyPort\=15236
  ```
  
* 旧的插件报了一个**&quot;re-registered bubbling event&quot;** 的错误，原因是`bubbling`与`direct`事件冲突：

  ```objc
  //RCT_EXPORT_VIEW_PROPERTY(onDismissed, RCTBubblingEventBlock);
  ```

  

## React

* `Antd`里的`TreeSelect`组件 ，如果开了自定义字段的话并且设置`showSearch`的时候，搜索不会生效的，还需要设置`treeNodeFilterProp`字段，例如

```javascript
showSearch: true,
filterTreeNode: true,
treeNodeFilterProp: &quot;name&quot;,
fieldNames: {
  label: &apos;name&apos;,
  value: &apos;id&apos;,
},
```



## Uniapp

* `UTS` 中 `number`调用`toByte()`方法转换为`byte`的时候要注意，传入的十进制是有符号的，也就是 -128 到 127范围，如果超出此范围会溢出。 



## Pjsip

* 生产环境上的一个BUG，如果通话的时候播放音乐，如果暂停音乐，然后再暂停通话就会闪退，原因是`mediaplayer`的资源释放，导致线程断言失败，因此在Pjsip里，资源的释放仅仅设置为null是没有用的，需要使用安全方法`delete()`
</content:encoded></item><item><title>程序员日记20241223</title><link>https://www.jason-z.com/posts/programmers-notes-20241223/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241223/</guid><pubDate>Mon, 23 Dec 2024 03:21:02 GMT</pubDate><content:encoded>



## React Native

* `Expo` 下要区分 ` Managed Workflow` 和 `Bare Workflow`, `Managed Workflow` 下默认不支持原生插件，需要`Eject`才能使用；

* 本来准备自己撸一个`RN`的优量汇的插件的，但是群内有一位米好心的老哥竟然免费分享给了我，十分感谢；

* 优量汇广告组件渲染的时候，还是要给与样式和高度，不然渲染不出来。

  

## Uniapp

* 遇到一个奇怪的问题，`nvue`里在`onLoad` 去调用 `UTS`组件的销毁方法时候，好像不会执行，组件的实例也没销毁，暂时的解决方法时候`UTS`组件本身的`NVUnloaded`事件里去销毁；
</content:encoded></item><item><title>程序员日记20241220</title><link>https://www.jason-z.com/posts/programmers-notes-20241220/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241220/</guid><pubDate>Fri, 20 Dec 2024 14:46:26 GMT</pubDate><content:encoded>



## Asterisk

* `AMI` 目前在生产环境中发现2个问题，1，操作的延时很大，2.很多Action不支持异步的，需要在应用层来实现。



## React Native

* `RN` 的国内生态这么不理想吗，连个优量汇这种广告SDK插件都没几个可以用的吗？



## Linux

* 一个奇怪的问题，添加主机信任之后，突然就访问不了，一直提示`connect refused`，然后从`authorized_keys`里删除公钥，又重新添加又可以了。。



## Uniapp

* `UTS`里如果回调数组的话传入`any[]` ，到前端会变成`object`类型，设置为`any`才会解析为数组。
</content:encoded></item><item><title>程序员日记20241219</title><link>https://www.jason-z.com/posts/programmers-notes-20241219/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241219/</guid><pubDate>Wed, 18 Dec 2024 18:03:54 GMT</pubDate><content:encoded>



## Android

* `jetpack compose`里的`checkbox` 避免使用 `remeber` 来存储局部状态的`checkedState` 或者也可以使用 `rememberUpdatedState`来实时更新；

* `jetpack compose`中软键盘和焦点的可以可以使用`keyboardcontroller`和`LocalFocusManager` 或 `FocusRequester` 进行管理，不过要注意`FocusRequester`必须要绑定在`modifer` 上，不然会闪退；

* 修改`imeAction = ImeAction.Send` 类型的时候，同样要注意在 `keyboardActions` 监听对应的 `onSend`, 其他类型同样的道理；

  

## Asterisk

* 设置`qualify`选项只是用来检测分机的电话状态，并不会自动取分机的注册信息；

* 如果使用`realtime`模式，可以通过`lastms`字段是否大于0，来判断是否注册状态；

  

## 其他

* 了解了一些关于`MIFARE`， `CPU卡`， `非接触IC卡`等相关名词的介绍
</content:encoded></item><item><title>程序员日记20241218</title><link>https://www.jason-z.com/posts/programmers-notes-20241218/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241218/</guid><pubDate>Tue, 17 Dec 2024 18:03:43 GMT</pubDate><content:encoded>



## Android

* 使用 **Android Studio** 将 `Aidl` 生成的`Java`类的时候，对于AGP 8+，将Aidl文件放置到**package**目录下**build**并不会直接生成，需要在**build.gradle**文件里设置：

  ```ini
  buildFeatures {
      aidl = true
  }
  ```

  

## Astro

* 备案被驳回了，因为如果多个域名指向同一个网址，需要显示不同的备案号，`SSG` 模式下不支持动态获取，所以只能还是用原生的`window.location.host` 来识别和显示。
</content:encoded></item><item><title>程序员日记20241217</title><link>https://www.jason-z.com/posts/programmers-notes-20241217/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241217/</guid><pubDate>Mon, 16 Dec 2024 21:14:55 GMT</pubDate><content:encoded>



## Android

* 安卓下的`BLE` 如果订阅数据的话，除了要设置`setCharacteristicNotification`为 `true`，另外必须要启用`CCC Descriptor `，以下是UTS代码示例（非kotlin代码）：

  ```typescript
  // 此处的00002902-0000-1000-8000-00805f9b34fb是固定值
  let descriptor = characteristic.getDescriptor(UUID.fromString(&quot;00002902-0000-1000-8000-00805f9b34fb&quot;))
  if (descriptor != null) {
    console.log(&quot;CCC Descriptor&quot;)
    descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
    bluetoothGatt?.writeDescriptor(descriptor)
    bluetoothGatt?.setCharacteristicNotification(characteristic, true)
  }
  ```

  

## Astro

* 给[博客](https://www.jason-z.com)直接加入百度统计代码，会有TS语法提示错误，可以通过`@ts-ignore`避免此问题

  ```js
    // @ts-ignore
  	 var _hmt: any[] = _hmt || [];
  ```

  



## 其他

* 偶然间翻了一下自己[51CTO的博客](https://blog.51cto.com/studiogang) 十几年前更新的文章，没想到人气还算不错的，如果坚持下去，可能应该会是头部的作者了吧，可惜事实是没有如果。。
</content:encoded></item><item><title>程序员日记20241216</title><link>https://www.jason-z.com/posts/programmers-notes-20241216/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241216/</guid><pubDate>Mon, 16 Dec 2024 06:21:57 GMT</pubDate><content:encoded>





## IOS

* `BLE`在调用`cancelPeripheralConnection`之后是无法收到 `didDisconnectPeripheral` 回调暂未解决；
* `BLE`连接的时候超时功能的暂未实现；
* `BLE`取消订阅后又重新订阅的问题暂未解决；
* `BLE`扫描的时候指定`withServices` 无法扫描到设备暂无解决；

*  初次尝试`NRE CONNECT`进行蓝牙辅助测试；

  

## Android

* 继续修改`BSIP SDK`相关问题，还是要花时间重构，要不然上层问题会越来越多！



## 其他

* 今天的状态不是太好，导致效率极低，可能是又有些焦虑了吧。
</content:encoded></item><item><title>程序员日记20241213</title><link>https://www.jason-z.com/posts/programmers-notes-20241213/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241213/</guid><pubDate>Fri, 13 Dec 2024 04:26:00 GMT</pubDate><content:encoded>
## Bun

* 昨天下载了一夜，依赖仍然没有成功，看来只能找下一下解决办法了，看了官网的文档，在`$HOME/.bunfig.toml` 全局配置源，类似`npm`配置

  ```ini
  [install]
  registry = &quot;https://registry.npmmirror.com/&quot;
  ```

  

## Astro

* 个人博客 [https://www.jason-z.com](https://www.jason-z.com) 初步搭建完成了，用了别人的主题稍微改了一下 ，2个小时左右就上线了，效果还是很不错的；

* 随便把公司的官网 [https://byteee.cn](https://byteee.cn) 也用`Astro`重构了一下，不过`Bussiness`相关的主题免费的就不多了，付费的要筛选，索性先拿着一个先用着，后面根据情况再完善吧；

* `Astro` 默认使用的是`static`模式，所以编译出来的`dist` 目录都是静态HTML文件，如果自己部署到`nginx`服务器上，可能直接访问一些路由诸如 /blog，会出现 403 / 404错误，因为实际编译出来的是blog.html，所以需要在nginx 上做一些解析配置：

  ```ini
  location / {
      try_files $uri $uri.html $uri/ /index.html;
  }
  
   # 兼容以 / 结尾的路径，将 /blog/ 重定向到对应的 index.html
  location ~ ^(.+)/$ {
      try_files $1/index.html $1.html $1/ /index.html;
  }
  ```

  

## Giscus

* Giscuss 初体验，配置比较简单，体验也不错，后期计划将其加入到[https://byteee.fund](https://byteee.fund) 里面去。



## SSL

* 今天看到公号上有一个推文，说`SSL`免费证书的有效期好像会进一步减少到45天，看来自动化续签的需求会进一步提升，看到有人推荐了`github`上的开源项目`domain admin` ，试用了一下还是不错，但是最后发现好像`1panel`基本上内置了这个功能了，所以可能暂时又用不到了。。



## 1Panel

* 作为一个传统的运维，所以对**宝塔** 这种的产品有点儿排斥。但是，这种观点有时候也会动摇的，后来看到了最近推荐了`1panel`，而且还是`JumpServer`的公司出的，之前做运维的时候和这家公司还有些交情，所以今天果断决定试试，没想到初次体验下来，感觉还是不错的，其他功能没有深入研究，至少这UI还是比较现代化的。
* 如果多个域名映射到一个网站上，并且都需要开启`https`的话，这里好像网站默认只会绑定一个`SSL`证书，导致其他域名的`https`访问失败，不知道是`1panel`功能上的BUG还是我使用的姿势不对，暂时的解决方案，就是自己手动修改`nginx` 配置， 通过不同的`Server Name`来映射证书。



## IOS

* `BLE`使用`retrieveConnectedPeripherals` 来进行查找已连接外设的时候，注意这里传入的services 是服务ID，而且测试的时候是需要必传的；
* `BLE`在调用`disconnect` 断开设备连接的时候，实际上调用的是应用级别的连接，系统级别的连接是无法断开的，这也很好解释为什么你调用这个方法之后，在IOS设备列表里仍然可以看到外设链接着。</content:encoded></item><item><title>程序员日记20241211</title><link>https://www.jason-z.com/posts/programmers-notes-20241211/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241211/</guid><pubDate>Wed, 11 Dec 2024 06:26:00 GMT</pubDate><content:encoded>
## Android

* 之前有一个`CamerX`相机遗留的历史问题一直没有解决，就是如何让拍照出来的照片和预览区域是一直的，由于我的预览区域的View是自定义的，没有固定的纵横比，所以我使用的裁剪模式方案，通过`FILL_CENTER`的缩放模式计算对应的坐标位置和裁剪区域裁剪出照片，其实原理知道了也不难，就是我忽略了一个拍摄出来图片是存在旋转的细节，导致前期折腾了很久。



## Uniapp

* `UTS`插件依赖的SDK可能会与UNIAPP自身的一些库冲突，看了一下官方的介绍，`config.json`配置里支持`exclude_group`配置，目前还在尝试中。



## 其他

* 其实以前我们对于二开项目都是拒绝的，但是如今渡过寒冬，有时候也不得不低头了 - -。</content:encoded></item><item><title>程序员日记20241211</title><link>https://www.jason-z.com/posts/programmers-notes-20241212/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241212/</guid><pubDate>Wed, 11 Dec 2024 06:26:00 GMT</pubDate><content:encoded>
## IOS

* `BLE`蓝牙折腾的一天，只能说之前学习的不够深入，只以为服务ID，特征值ID就可以操作了，结果忽略了特征值ID 是有不同的`properties`的，需要根据`property`的不同，判断是否具备`.write` ，`.notify`，`.read`属性来进行不同的获取值的方法，否则，就会造成你以为自己发送了指令，但在回调里获取不到值的情况。
* 学习了`Xcode`辅助工具`Packetlogger`进行蓝牙抓包，确实好用。



## Astro

* 在继续对比了一番之后，还是放弃了原来自己的`nextjs`+ `headless cms` 搭建博客的方案，最终选择了`Astro`+`Markdown`，从今天开始折腾。

* 首次接触到了`bun.sh`这个工具，据说比`NodeJS` 快，然而同样的情况下，我的依赖下载了一夜。。

</content:encoded></item><item><title>程序员日记20241210</title><link>https://www.jason-z.com/posts/programmers-notes-20241210/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241210/</guid><pubDate>Tue, 10 Dec 2024 06:26:00 GMT</pubDate><content:encoded>
## Android

* **org.gradle.api.GradleException** 编译错误。

解决方案：gradle版本过低，升级到7.5以上即可

```
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
```

```groovy
    dependencies {
        classpath &apos;com.android.tools.build:gradle:7.2.2&apos;
        ....
```



* **Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE** 编译错误

​	解决方案：升级**work:work-runtime** 依赖版本到2.7+

       ```groovy
        implementation &apos;androidx.work:work-runtime:2.7.0&apos;
       ```



## UTS

* `uniapp` 虽然默认依赖了轮子哥的`XXPermission`让权限申请变的简单，但是如果用户去处理诸如蓝牙，存储等一些权限权限的时候，还必须考虑版本的适配性，因此还是有诸多不便，所以不知道是否有类似的原生库在做这个事情不？如果没有，到时候自己空了撸一个。
* 有个客户咨询了，是否可以在安卓下获取到运行进程的集合，从Android 7.0以后因为安全限制的问题已经不行了，不知道是否有其他黑魔法？



## 其他

* 今天感觉是工作效率是比较高的，交付了原生SDK项目，另外把拖了好久的`flutter`蓝牙定制插件项目的初版做了出来，后面一下午专心研究了一个手环项目的DEMO，把整个流程大致上都熟悉得差不多了，后面写安卓的接口基本上就用了半天+一晚上，看来还是要在前期要多下功夫，后面开发效率也会高很多。</content:encoded></item><item><title>程序员日记20241209</title><link>https://www.jason-z.com/posts/programmers-notes-20241209/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241209/</guid><pubDate>Mon, 09 Dec 2024 06:26:00 GMT</pubDate><content:encoded>

## Android

* 被**无障碍服务**折磨了一晚上，理论上APP杀死后，服务应该还在，但是有时候服务直接就没了，估计是因为华为，小米这些机器的安全机制吧，所以无障碍服务如何保活，其实也是一个技术课题。
* 继续尝试研究了一下**全局监听手势事件**，看到有几个方案通过遍历`view` + 代理`onTouchListner`的方案，但是实际尝试了，好像并没有效果，由于时间原因，没有继续研究下去了，还是切回了`activity`里回调的方案。



## IOS

* IOS**陀螺仪**启动后没有回调，刚开始以为是需要啥权限的，调试了半天，后来发现静态类中的局部变量的问题导致实例被回收了。

* IOS下的**全局手势事件**，目前是通过自定义`window` 然后重新`onEvent`方法来实现的。

  

## UTS

* `UTSActivityCallback` 只支持`uniappx`，因此在`uniapp`下目前应该无法监听到`activity`的回调事件。



## 其他

* 之前接了一个项目，代理人没有表达清楚，以为是要做`uniapp`的插件，结果花费了大量的时间去找寻`uniapp`的适配方案，结果后来跟客户说的是要做原生SDK，下次还是要沟通和明确好项目需求再进行。</content:encoded></item><item><title>程序员日记20241206</title><link>https://www.jason-z.com/posts/programmers-notes-20241206/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241206/</guid><pubDate>Fri, 06 Dec 2024 06:26:00 GMT</pubDate><content:encoded>


## Android

* 如何优雅的重启安卓App，尝试了一些代码效果不理想，结果还是用了[*ProcessPhoenix*](https://github.com/JakeWharton/ProcessPhoenix) 这个库，一行代码搞定。

* 又是被`JNI`折磨的一天，厂商给了一个少参数的.h文件，结果JAVA调用就报错了，我尝试`nm`和`objdump`来分析`so`文件，但是只能分析出函数名，无法获取完整参数，有知道的大佬告知一下。

## Asterisk

* 修改conf文件之后，调用`Asterisk` 的`AMI` 的 `reload`命令 好像并没有生效，但是CLI模式下的`core reload`就可以，今天时间不够，晚点儿看一下源码研究一下。

## 工作流

* 准备实现一个可以自动同步到文章到各大平台的工具（自用），简单看了一下工作流网站[Zapier](https://zapier.com/)是真不错，但是对国内的APPs支持就差了点儿，最终还是打算用`python` + `actionscript` + `playwright`来从头撸吧。

## UTS插件

* 客户咨询了一个做电话呼叫转移插件的功能，简单看了一下安卓`API`是有的，但是不太清楚对手机和SIM卡的适配性如何。
* 如果修改了`UTSAndroidHookProxy`代码，切记要重新自定义基座，才能生效！！！

</content:encoded></item><item><title>程序员日记20241205</title><link>https://www.jason-z.com/posts/programmers-notes-20241205/</link><guid isPermaLink="true">https://www.jason-z.com/posts/programmers-notes-20241205/</guid><pubDate>Thu, 05 Dec 2024 06:26:00 GMT</pubDate><content:encoded>


## UTS插件

1. `IOS`中的**字典** `[String: any]` 对应的是 `UTS`中的**Map** `new Map&lt;String, any&gt;`。

2. `UTS`插件安卓的`so`文件不支持本地调试，需打包为`aar`或 `jar`

## Android Studio

1. **2024.1.1**版本中默认创建工程中使用的`junit`版本是`4.14-snapshot`，但实际中各大仓库并没有此版本，可修改为 `4.13.2`。

</content:encoded></item><item><title>JDK17 与 ButterKinife 冲突 class butterknife.compiler.ButterKnifeProcessor$RScanner的解决方案</title><link>https://www.jason-z.com/posts/jdk17-butterknife-conflict-class-butterknife-compiler-processor-rscanner-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/jdk17-butterknife-conflict-class-butterknife-compiler-processor-rscanner-solution/</guid><pubDate>Thu, 21 Nov 2024 08:17:07 GMT</pubDate><content:encoded>

## 概述

在编译 Android Studio 项目时，可能会遇到以下错误提示：

```
Cause: superclass access check failed: class butterknife.compiler.ButterKnifeProcessor$RScanner (in unnamed module @0x274412b0) cannot access class com.sun.tools.javac.tree.TreeScanner (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.tree to unnamed module @0x274412b0
```

该错误通常表示 `ButterKnife` 的处理器类无法访问 JDK 编译器模块中的某些内部类。本文将介绍两种解决方法。

## 解决办法

### 一：降低 JDK 版本

如果使用的是 JDK 17，可以尝试将 JDK 版本降为 JDK 11 或 JDK 15。较低版本的 JDK 可能不会出现此类问题。

#### 步骤

1. **下载并安装较低版本的 JDK**：
   - 访问 [Oracle JDK 下载页面](https://www.oracle.com/java/technologies/javase-downloads.html) 或使用 [AdoptOpenJDK](https://adoptopenjdk.net/) 获取较低版本的 JDK。

2. **配置 Android Studio 使用新版本的 JDK**：
   - 打开 Android Studio，点击菜单栏中的 `File` -&gt; `Project Structure`。
   - 在 `Project` 标签下选择新安装的 JDK 版本。

### 二：修改 `gradle.properties` 文件

可以在项目的根目录下的 `gradle.properties` 文件中添加特定的 JVM 参数，以允许访问所需的内部类。

#### 步骤

1. **编辑 `gradle.properties` 文件**：
   - 打开项目根目录下的 `gradle.properties` 文件。

2. **添加以下配置**：

   ```properties
   org.gradle.jvmargs=-Xmx1920M \
       --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
       --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
       --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
   ```

3. **保存文件并重新构建项目**：
   - 保存更改后，重新构建项目以确保设置生效。



</content:encoded></item><item><title>Go程序编译出错：Load redeclared in this block</title><link>https://www.jason-z.com/posts/go-complier-error-load-redeclared-in-this-block/</link><guid isPermaLink="true">https://www.jason-z.com/posts/go-complier-error-load-redeclared-in-this-block/</guid><pubDate>Tue, 22 Oct 2024 05:29:37 GMT</pubDate><content:encoded>
## 概述

在升级 Go 版本后，编译一个 Go 模块时可能会遇到以下错误提示：

```
# runtime/internal/atomic
xxx\src\runtime\internal\atomic\atomic_amd64x.go:13:6: Load redeclared in this block
xxx\src\runtime\internal\atomic\atomic_amd64x.go:19:6: Loadp redeclared in this block
    previous declaration at xxx\src\runtime\internal\atomic\atomic_amd64.go:22:32
    previous declaration at xxx\src\runtime\internal\atomic\atomic_amd64.go:28:26
xxx\src\runtime\internal\atomic\atomic_amd64x.go:30:6: Xadd redeclared in this block
    previous declaration at xxx\src\runtime\internal\atomic\atomic_amd64.go:39:37
xxx\src\runtime\internal\atomic\atomic_amd64x.go:33:6: Xadd64 redeclared in this block
    previous declaration at xxx\src\runtime\internal\atomic\atomic_amd64.go:42:39
xxx\src\runtime\internal\atomic\atomic_amd64x.go:36:6: Xadduintptr redeclared in this block
    previous declaration at xxx\src\runtime\internal\atomic\atomic_amd64.go:45:47
    previous declaration at xxx\src\runtime\internal\atomic\atomic_amd64.go:48:36
xxx\src\runtime\internal\atomic\atomic_amd64x.go:42:6: Xchg64 redeclared in this block
xxx\src\runtime\internal\atomic\atomic_amd64x.go:45:6: Xchguintptr redeclared in this block
    previous declaration at xxx\src\runtime\internal\atomic\atomic_amd64.go:54:45
xxx\src\runtime\internal\atomic\atomic_amd64x.go:48:6: And8 redeclared in this block
    previous declaration at xxx\src\runtime\internal\atomic\atomic_amd64.go:63:27
xxx\src\runtime\internal\atomic\atomic_amd64x.go:48:6: too many errors
```

这些错误表示某些函数或变量在同一作用域内被重复声明。

## 问题原因

出现这个问题的原因是因为在升级 Go 版本时直接覆盖了旧版本的文件，导致新旧版本的源码文件共存，从而引发重复声明的错误。

## 解决办法

### 方法一：删除旧的 Go 安装目录并重新安装

1. **删除旧的 Go 安装目录**：
   - 找到并删除旧版本的 Go 安装目录（通常位于 `/usr/local/go` 或其他自定义路径）。

   ```bash
   sudo rm -rf /usr/local/go
   ```

2. **重新安装最新版本的 Go**：
   - 下载并安装最新版本的 Go。可以从 [Go 官方下载页面](https://golang.org/dl/) 获取安装包。
   
   ```bash
   wget https://go.dev/dl/go&lt;version&gt;.linux-amd64.tar.gz
   sudo tar -C /usr/local -xzf go&lt;version&gt;.linux-amd64.tar.gz
   ```

3. **更新环境变量**：
   - 确保 `PATH` 环境变量包含新的 Go 安装路径。

   ```bash
   export PATH=$PATH:/usr/local/go/bin
   ```

   将上述命令添加到 `~/.bashrc` 或 `~/.zshrc` 文件中以确保每次登录都生效。

### 方法二：清理 Go 模块缓存

有时，模块缓存中的旧版本文件也可能导致冲突。可以尝试清理 Go 模块缓存：

```bash
go clean -modcache
```

</content:encoded></item><item><title>Uniapp离线打包IOS工程的几个坑</title><link>https://www.jason-z.com/posts/uniapp-ios-offline-archive-bugs/</link><guid isPermaLink="true">https://www.jason-z.com/posts/uniapp-ios-offline-archive-bugs/</guid><pubDate>Sun, 20 Oct 2024 11:47:03 GMT</pubDate><content:encoded>



从官网下载的uniapp ios的sdk工程运行时可能会遇到以下几个问题，需要进行修改：

## 1. HBuilder 的签名配置冲突

![image.png](https://backend.jason-z.com/uploads/image_5cf1c900cc.png)

**问题描述：**
工程里有一些遗留的签名配置信息。

**解决办法：**
通过Build setting中搜索Provision，删除对应配置信息即可。

## 2. 安装Pod依赖时的警告信息

```plaintext
[!] The `HBuilder [Debug]` target overrides the `GCC_PREPROCESSOR_DEFINITIONS` build setting defined in `Pods/Target Support Files/Pods-HBuilder/Pods-HBuilder.debug.xcconfig&apos;. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.
...
```

**问题描述：**
多个构建设置被覆盖，导致CocoaPods安装出现问题。

**解决办法：**

- 在项目 Build Settings 中：
  - **Other Linker Flags**：增加 `$(inherited)`，保留 `-ObjC`。
  - **Library Search Paths**：增加 `$(inherited)`。
  - **Preprocessor Macros**：增加 `$(inherited)`。

然后重新运行 `pod update` 命令。

## 3. Sandbox 错误：rsync.samba in Xcode project

![image.png](https://backend.jason-z.com/uploads/image_cb3448d850.png)

**问题描述：**
在Xcode项目中出现Sandbox错误。

**解决办法：**
修改Build Setting中的 `ENABLE_USER_SCRIPT_SANDBOXING` 的值为 `NO`。

## 4. 编译警告：&quot;Mixed ObjC ABI, xxx compiled without category class properties&quot;

![image.png](https://backend.jason-z.com/uploads/image_9e10f637cd.png)

**问题描述：**
编译时出现混合ObjC ABI警告。

**解决办法：**
在Build Setting的 **Other Linker Flag** 中增加 `-ld_classic` 选项。


</content:encoded></item><item><title>网页录音提示 TypeError Cannot read property &apos;getUserMedia&apos; of undefined 错误的解决方法</title><link>https://www.jason-z.com/posts/type-error-cannot-read-property-get-user-media-of-undefined/</link><guid isPermaLink="true">https://www.jason-z.com/posts/type-error-cannot-read-property-get-user-media-of-undefined/</guid><pubDate>Tue, 15 Oct 2024 21:25:03 GMT</pubDate><content:encoded>
## 简介

在使用 JavaScript 进行网页录音时，本地调试可能正常工作，但在部署到正式环境后可能会遇到以下错误：

```
TypeError: Cannot read property &apos;getUserMedia&apos; of undefined
```

这是由于 Chrome 对安全性的限制导致的。`navigator.mediaDevices.getUserMedia` 只能在 HTTPS 环境下或本地开发环境中（如 `localhost`）正常使用。

## 解决办法

### 方法一：更改正式环境支持 HTTPS

最推荐的做法是确保你的正式环境支持 HTTPS。HTTPS 提供了更安全的通信协议，避免了许多浏览器的安全限制。

### 方法二：修改浏览器配置

如果你暂时无法将正式环境迁移到 HTTPS，可以通过修改浏览器配置来绕过此限制。请注意，这种方法仅适用于开发和测试环境，不建议在生产环境中使用。

#### 步骤：

1. **打开 Chrome 实验性标志页面**：
   在浏览器地址栏中输入以下 URL 并回车：
   
   ```
   chrome://flags/#unsafely-treat-insecure-origin-as-secure
   ```

2. **启用不安全来源的安全处理**：
   找到 `Insecure origins treated as secure` 选项，并将其设置为启用状态。
   
   ![修改浏览器配置](https://backend.jason-z.com/uploads/image_2094aa06e7.png)

3. **添加需要信任的域名**：
   在弹出的输入框中添加你希望信任的不安全域名，例如 `http://yourdomain.com`。

4. **重启浏览器**：
   修改完成后，记得重启浏览器以使更改生效。





</content:encoded></item><item><title>Xcode 15 提示create a multiple project then pod install 错误的解决办法</title><link>https://www.jason-z.com/posts/xcode-15-create-a-multiple-project-then-pod-install/</link><guid isPermaLink="true">https://www.jason-z.com/posts/xcode-15-create-a-multiple-project-then-pod-install/</guid><pubDate>Tue, 15 Oct 2024 19:42:38 GMT</pubDate><content:encoded>

## 概述

在使用 Xcode 15 编译项目时，可能会遇到以下错误提示：

```plaintext
create a multiple project then pod install
realpath: illegal option -- m usage: realpath [-q] [path ...] :20: error: Unexpected failure
```

本文将介绍如何解决这一问题。

## 错误描述

编译过程中出现如下错误信息：

```plaintext
create a multiple project then pod install
realpath: illegal option -- m usage: realpath [-q] [path ...] :20: error: Unexpected failure
```

## 解决办法

### 修改 Build Settings

1. 打开你的 iOS 工程。
2. 进入项目的 **Build Settings**。
3. 找到并修改 `ENABLE_USER_SCRIPT_SANDBOXING` 设置为 `NO`。

具体步骤如下：

- 在 Xcode 中选择你的项目文件。
- 点击左侧栏中的目标（Target）。
- 选择 **Build Settings** 标签页。
- 使用搜索框查找 `ENABLE_USER_SCRIPT_SANDBOXING`。
- 将其值设置为 `NO`。

### 示例截图

![修改 Build Settings](https://example.com/path/to/your/screenshot.png)  
*请替换为实际的截图链接*



</content:encoded></item><item><title>Xcode 编译提示 Missing file libarclite_iphoneos.a错误的解决办法</title><link>https://www.jason-z.com/posts/xcode-mssing-file-libarclite_iphoneosa-error-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/xcode-mssing-file-libarclite_iphoneosa-error-solution/</guid><pubDate>Tue, 15 Oct 2024 19:33:31 GMT</pubDate><content:encoded>

## 概述

在使用 Xcode 编译项目时，可能会遇到以下错误提示：

```plaintext
File not found: /Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphoneos.a
```

本文将介绍如何解决这一问题。

## 错误描述

编译过程中出现如下错误信息：

```plaintext
File not found: /Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphoneos.a
```

## 解决办法

### 修改 Podfile

为了解决这个问题，需要在你的 `Podfile` 中添加相应的配置。根据项目的最小 iOS 编译版本（如 11.0、12.0 等），设置 `IPHONEOS_DEPLOYMENT_TARGET`。例如，对于最小支持版本为 13.0 的项目，可以在 `Podfile` 中加入如下内容：

#### 对于普通项目

```ruby
post_install do |installer|
  installer.generated_projects.each do |project|
    project.targets.each do |target|
      target.build_configurations.each do |config|
        config.build_settings[&apos;IPHONEOS_DEPLOYMENT_TARGET&apos;] = &apos;13.0&apos;
      end
    end
  end
end
```

#### 对于 Flutter 项目

```ruby
post_install do |installer|
  installer.generated_projects.each do |project|
    project.targets.each do |target|
      target.build_configurations.each do |config|
        config.build_settings[&apos;IPHONEOS_DEPLOYMENT_TARGET&apos;] = &apos;13.0&apos;
      end
    end
  end
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
  end
end
```






</content:encoded></item><item><title>Xcode 错误提示 swiftCompatibility50  错误的解决办法</title><link>https://www.jason-z.com/posts/xcode-swiftcompatibility50-build-error-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/xcode-swiftcompatibility50-build-error-solution/</guid><pubDate>Tue, 15 Oct 2024 18:39:10 GMT</pubDate><content:encoded>

## 概述

在使用 Xcode 编译项目时，可能会遇到以下错误提示：

```plaintext
Undefined symbols for architecture x86_64:
 &quot;__swift_FORCE_LOAD_$_swiftCompatibility50&quot;, referenced from:
     __swift_FORCE_LOAD_$_swiftCompatibility50_$_Lottie in liblottie-ios.a(AnimatedButton.o)
     __swift_FORCE_LOAD_$_swiftCompatibility50_$_Lottie in liblottie-ios.a(AnimatedControl.o)
     __swift_FORCE_LOAD_$_swiftCompatibility50_$_Lottie in liblottie-ios.a(AnimationSubview.o)
     __swift_FORCE_LOAD_$_swiftCompatibility50_$_Lottie in liblottie-ios.a(CompatibleAnimationView.o)
     __swift_FORCE_LOAD_$_swiftCompatibility50_$_Lottie in liblottie-ios.a(CompositionLayer.o)
     __swift_FORCE_LOAD_$_swiftCompatibility50_$_Lottie in liblottie-ios.a(NullCompositionLayer.o)
     __swift_FORCE_LOAD_$_swiftCompatibility50_$_Lottie in liblottie-ios.a(ImageCompositionLayer.o)
     ...
(maybe you meant: 
__swift_FORCE_LOAD_$_swiftCompatibility50_$_lottie_react_native, 
__swift_FORCE_LOAD_$_swiftCompatibility50_$_Lottie )
```

本文将介绍如何解决这一问题。

## 错误描述

编译过程中出现如下错误信息：

```plaintext
Undefined symbols for architecture x86_64:
 &quot;__swift_FORCE_LOAD_$_swiftCompatibility50&quot;, referenced from:
     ...
```

## 解决办法

### 恢复 Swift 文件

默认的 iOS 工程会创建 Swift 文件，但可能由于某些原因被无意删除。解决方法是恢复这个 Swift 文件，并确保其包含基本的 Swift 代码。具体步骤如下：

1. 在项目中添加一个新的 Swift 文件（例如 `Empty.swift`）。
2. 确保该文件包含以下内容：

```swift
import Foundation
```








</content:encoded></item><item><title>安卓通过MediaDrm获取唯一设备ID</title><link>https://www.jason-z.com/posts/android-get-unique-device-id-via-mediadrm/</link><guid isPermaLink="true">https://www.jason-z.com/posts/android-get-unique-device-id-via-mediadrm/</guid><pubDate>Tue, 27 Aug 2024 05:46:54 GMT</pubDate><content:encoded>

## 概述

在某些应用场景中，获取设备的唯一标识符（Device ID）是非常重要的，例如用于用户身份验证或设备绑定。从 Android 8.0（API 级别 26）开始，传统的获取设备 ID 的方法（如 `IMEI`、`MEID` 或 `Android ID`）受到了更严格的限制。因此，推荐使用 `MediaDrm` 来获取一个可靠的设备唯一标识符。

## 实现代码

以下是如何通过 `MediaDrm` 获取设备唯一标识符的实现代码：

### 使用 MediaDrm 获取设备唯一标识符

```java
/**
 * 通过 MediaDrm 获取设备唯一标识符
 */
private String getUniqueDeviceId(Context context) {
    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.M) {
        try {
            // 创建 MediaDrm 对象
            MediaDrm mediaDrm = new MediaDrm(C.WIDEVINE_UUID);

            // 获取设备唯一标识符
            byte[] deviceUniqueId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);

            // 将字节数组转换为十六进制字符串
            StringBuilder deviceIdHex = new StringBuilder();
            for (byte b : deviceUniqueId) {
                deviceIdHex.append(String.format(&quot;%02x&quot;, b));
            }

            // 释放资源
            mediaDrm.release();

            return deviceIdHex.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    } else {
        // 对于低于 API 23 的设备，可以考虑其他方式获取唯一标识符
        Log.w(&quot;getUniqueDeviceId&quot;, &quot;MediaDrm is not supported on API &lt; 23.&quot;);
        return null;
    }
}
</content:encoded></item><item><title>android端和Java端RSA加解密不一致的解决办法</title><link>https://www.jason-z.com/posts/android-java-rsa-encryption-decryption-inconsistency-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/android-java-rsa-encryption-decryption-inconsistency-solution/</guid><pubDate>Tue, 27 Aug 2024 04:16:33 GMT</pubDate><content:encoded>

## 概述

在开发过程中，有时会遇到 Android 端和 Java 端（如 Spring Boot）使用 RSA 算法进行加解密时出现不一致的问题。具体表现为：在服务端加密的数据，在 Android 端解密时失败。

### 问题原因

主要原因是两端使用的 RSA 标准不同，导致加解密算法不匹配。为了解决这一问题，需要确保两端使用相同的加密标准。

## 解决方案

### 统一加密标准

#### 1. Android 端

在 Android 端应使用以下标准进行加解密操作：

```java
Cipher cipher = Cipher.getInstance(&quot;RSA/ECB/PKCS1Padding&quot;);
```

#### 2. 服务端（Java）

在服务端（如 Spring Boot）应使用相同的加密标准：

```java
Cipher cipher = Cipher.getInstance(&quot;RSA/ECB/PKCS1Padding&quot;);
```

### 示例代码

#### Android 端解密示例

```java
public String decrypt(String encryptedData, PrivateKey privateKey) throws Exception {
    Cipher cipher = Cipher.getInstance(&quot;RSA/ECB/PKCS1Padding&quot;);
    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    byte[] decryptedBytes = cipher.doFinal(Base64.decode(encryptedData, Base64.DEFAULT));
    return new String(decryptedBytes, StandardCharsets.UTF_8);
}
```

#### 服务端（Java）加密示例

```java
public String encrypt(String plainText, PublicKey publicKey) throws Exception {
    Cipher cipher = Cipher.getInstance(&quot;RSA/ECB/PKCS1Padding&quot;);
    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
    return Base64.getEncoder().encodeToString(encryptedBytes);
}
```

### 注意事项

- **加密标准一致性**：确保两端使用相同的加密标准（如 `RSA/ECB/PKCS1Padding`），以避免加解密不一致的问题。
- **Base64 编码**：在传输加密数据时，通常使用 Base64 编码以确保数据格式正确。
- **字符编码**：确保在加密和解密过程中使用相同的字符编码（如 UTF-8），以避免字符集不一致导致的问题。



</content:encoded></item><item><title>react native 提示 &quot;bundleReleaseJsAndAssets  non-zero exit &quot; 错误的解决办法</title><link>https://www.jason-z.com/posts/react-native-app-bundlereleasejsandassets-no-zero-exit-error/</link><guid isPermaLink="true">https://www.jason-z.com/posts/react-native-app-bundlereleasejsandassets-no-zero-exit-error/</guid><pubDate>Sat, 10 Aug 2024 06:33:10 GMT</pubDate><content:encoded>


## 简介

在打包 React Native APK 时，可能会遇到以下错误提示：

```
Execution failed for task &apos;:app:bundleReleaseJsAndAssets&apos;.
Process &apos;command &apos;../../node_modules/hermes-engine/linux64-bin/hermes&apos;&apos; finished with non-zero exit value 2

Error: Command failed: ./gradlew app:installRelease -PreactNativeDevServerPort=8081

/xxxx/android/app/build/generated/assets/react/release/index.android.bundle:1:1: error: Invalid UTF-8 continuation byte �� ���
/xxxx/android/app/build/generated/assets/react/release/index.android.bundle:1:1: error: unrecognized Unicode character \ufffd �� ���
...
```

## 错误原因

根据 [React Native 官方文档](https://reactnative.dev/docs/ram-bundles-inline-requires) 的说明，如果你使用了 Hermes JS 引擎，则不应启用 RAM Bundles 功能。Hermes 在加载字节码时使用 `mmap` 确保整个文件不会被加载，而 RAM Bundles 与此机制不兼容，可能会导致问题。

&gt; 如果你使用 Hermes JS 引擎，你不应启用 RAM Bundles 功能。在 Hermes 中，加载字节码时，`mmap` 确保整个文件不会被加载。使用 Hermes 与 RAM Bundles 可能会导致问题，因为这些机制是不兼容的。

## 解决办法

### 方法一：关闭 RAM Bundles 配置

注释掉或删除 `app/build.gradle` 文件中的 RAM Bundles 相关配置：

```groovy
// bundleCommand = &quot;ram-bundle&quot;
// extraPackagerArgs = [&quot;--indexed-ram-bundle&quot;]
```

### 方法二：关闭 Hermes JS 引擎

如果不需要使用 Hermes，可以在 `android/app/build.gradle` 文件中禁用它：

```groovy
project.ext.react = [
    entryFile: &quot;index.js&quot;,
    enableHermes: false  // 禁用 Hermes 引擎
]
```

### 验证配置

1. **清理构建缓存**：

   ```bash
   cd android
   ./gradlew clean
   ```

2. **重新打包 APK**：

   ```bash
   npx react-native run-android --variant=release
   ```

</content:encoded></item><item><title>android 提示 gradle been locked by this process错误的解决方法</title><link>https://www.jason-z.com/posts/android-gradle-been-locked-by-this-process-error/</link><guid isPermaLink="true">https://www.jason-z.com/posts/android-gradle-been-locked-by-this-process-error/</guid><pubDate>Fri, 09 Aug 2024 08:12:51 GMT</pubDate><content:encoded>

## 概述

在开发 Android 项目时，有时会遇到以下错误提示：

```
Cannot lock execution history cache (/xxx/android/.gradle/8.2/executionHistory) as it has already been locked by this process.
```

该错误通常表示 Gradle 缓存文件被当前进程锁定，导致无法继续编译。本文将介绍如何解决这一问题。

## 解决办法

### 方法一：终止 Gradle 守护进程

最直接的方法是终止所有正在运行的 Gradle 守护进程。可以通过以下命令实现：

```bash
pkill -f &apos;.*GradleDaemon.*&apos;
```

此命令会强制终止所有与 Gradle 守护进程相关的进程，释放被锁定的缓存文件。

### 方法二：手动删除锁定文件

如果上述方法无效，可以尝试手动删除锁定文件。具体步骤如下：

1. 找到 `.gradle` 目录下的 `executionHistory.lock` 文件。
2. 删除该文件：
   ```bash
   rm -f /path/to/project/.gradle/8.2/executionHistory/executionHistory.lock
   ```

请注意，删除锁定文件可能会导致缓存失效，但不会影响项目的正常编译。

### 方法三：重启 IDE 或计算机

如果以上两种方法均未解决问题，建议尝试重启集成开发环境（IDE）或计算机。这有助于清理所有可能的进程锁定问题。


</content:encoded></item><item><title>react native 提示Unable to load script.Make错误的解决方法</title><link>https://www.jason-z.com/posts/react-native-unable-to-load-script-make-error/</link><guid isPermaLink="true">https://www.jason-z.com/posts/react-native-unable-to-load-script-make-error/</guid><pubDate>Fri, 09 Aug 2024 08:09:21 GMT</pubDate><content:encoded>


## 简介

在安卓真机上调试 React Native 项目时，可能会遇到以下错误提示：

&gt; Unable to load script. Make sure you are either running a Metro server or that your bundle &apos;index.android.bundle&apos; is packaged correctly for release.

![调试错误](https://backend.jason-z.com/uploads/v_CZ_Zs_ddb56c071e.png)

## 解决办法

### 启动 Metro Bundler

确保 Metro Bundler 正在运行。可以通过以下命令启动：

```bash
npx react-native start
```

### 设置端口转发

为了使真机能够访问本地开发服务器，需要设置端口转发。使用以下命令将本地端口 8081 转发到设备：

```bash
adb reverse tcp:8081 tcp:8081
```

### 验证配置

1. **检查 Metro Bundler**：确保 Metro Bundler 没有报错并正在监听端口 8081。
2. **重启应用**：在设备上重新启动应用，确保应用能够正确加载脚本。

</content:encoded></item><item><title>java.lang.NoClassDefFoundError ImagePerfDataListener 错误的解决方案</title><link>https://www.jason-z.com/posts/java-lang-no-class-def-found-error-image-perf-data-listener/</link><guid isPermaLink="true">https://www.jason-z.com/posts/java-lang-no-class-def-found-error-image-perf-data-listener/</guid><pubDate>Fri, 09 Aug 2024 07:59:07 GMT</pubDate><content:encoded>
## 概述

在 Android 开发中，使用 `ContentResolver` 通过 URI 获取文件路径时，可能会遇到以下错误：

```
java.lang.IllegalArgumentException: column &apos;_data&apos; does not exist
```

主要原因是从 API 29 开始，`MediaStore.Files.FileColumns.DATA` 字段已经被废弃。因此，直接通过 `_data` 列获取文件路径的方法不再适用。

## 原因分析

从 Android 10（API 级别 29）开始，Google 强制执行了更严格的存储权限策略，`MediaStore` 中的 `_data` 列被废弃，取而代之的是推荐使用 `FileDescriptor` 来读取文件内容。

## 旧版代码

之前我们使用的通过 URI 获取文件路径的代码如下：

```java
private void getContentResolverInfo(Uri uri, int width, int height, SlideFactory.MediaType mediaType) {
    Cursor cursor = null;
    long start = System.currentTimeMillis();
    try {
        cursor = context.getContentResolver().query(uri, null, null, null, null);
        if (cursor != null &amp;&amp; cursor.moveToFirst()) {
            String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
            long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
            long filePath = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
            String mimeType = context.getContentResolver().getType(uri);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (cursor != null)
            cursor.close();
    }
}
```

## 修改后的代码

为了解决上述问题，修改后的代码如下：

```java
private void getContentResolverInfo(Uri uri, int width, int height, SlideFactory.MediaType mediaType) {
    Cursor cursor = null;
    long start = System.currentTimeMillis();
    try {
        cursor = context.getContentResolver().query(uri, null, null, null, null);
        if (cursor != null &amp;&amp; cursor.moveToFirst()) {
            String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
            long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));

            // 使用 FileDescriptor 读取文件内容
            ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, &quot;r&quot;);
            if (parcelFileDescriptor != null) {
                FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
                String keystoreContent = readFileContent(fileDescriptor);
                FileInputStream fileInputStream = new FileInputStream(fileDescriptor);
                String mimeType = context.getContentResolver().getType(uri);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (cursor != null)
            cursor.close();
    }
}

public static String readFileContent(FileDescriptor fileDescriptor) {
    Log.i(TAG, &quot;readFileContent(), fileDescriptor=&quot; + fileDescriptor);
    if (fileDescriptor == null) {
        return null;
    }

    StringBuilder sb = new StringBuilder();
    try (FileInputStream fileInputStream = new FileInputStream(fileDescriptor);
         InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
         BufferedReader reader = new BufferedReader(inputStreamReader)) {

        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line).append(&quot;\n&quot;);
        }
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }

    return sb.toString();
}
```

</content:encoded></item><item><title>el-tree 实现折叠全部和全部功能</title><link>https://www.jason-z.com/posts/el-tree-expand-all-and-collpase-all/</link><guid isPermaLink="true">https://www.jason-z.com/posts/el-tree-expand-all-and-collpase-all/</guid><pubDate>Mon, 15 Jul 2024 07:51:59 GMT</pubDate><content:encoded>
## 概述

在使用 `el-tree` 组件时，有时需要实现“折叠全部”和“展开全部”的功能。本文将介绍如何通过 Vue 和 Element UI 实现这一功能。

## 示例代码

### HTML + Vue 代码

```vue
&lt;template&gt;
  &lt;div&gt;
    &lt;el-switch
      v-model=&quot;switchValue&quot;
      @change=&quot;switchChange&quot;
      active-color=&quot;#13ce66&quot;
      inactive-color=&quot;#ff4949&quot;
    &gt;
    &lt;/el-switch&gt;
    &lt;el-tree
      ref=&quot;treeRef&quot;
      :props=&quot;props&quot;
      :data=&quot;treeData&quot;
      node-key=&quot;id&quot;
      show-checkbox
      @check-change=&quot;handleCheckChange&quot;
      :default-expand-all=&quot;isExpand&quot;
    &gt;
    &lt;/el-tree&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      switchValue: false,
      isExpand: false,
      props: {
        label: &apos;label&apos;,
        children: &apos;children&apos;
      },
      treeData: [
        // 树形数据结构示例
        {
          id: 1,
          label: &apos;一级 1&apos;,
          children: [
            {
              id: 4,
              label: &apos;二级 1-1&apos;,
              children: [
                {
                  id: 9,
                  label: &apos;三级 1-1-1&apos;
                },
                {
                  id: 10,
                  label: &apos;三级 1-1-2&apos;
                }
              ]
            }
          ]
        },
        // 更多树节点...
      ]
    };
  },
  methods: {
    switchChange() {
      this.isExpand = !this.isExpand;
      this.$nextTick(() =&gt; {
        this.updateTreeExpansion();
      });
    },
    updateTreeExpansion() {
      const treeList = this.treeData;
      for (let i = 0; i &lt; treeList.length; i++) {
        this.expandNode(treeList[i]);
      }
    },
    expandNode(node) {
      if (this.$refs.treeRef.store.nodesMap[node.id]) {
        this.$refs.treeRef.store.nodesMap[node.id].expanded = this.isExpand;
      }
      if (node.children &amp;&amp; node.children.length &gt; 0) {
        node.children.forEach(child =&gt; this.expandNode(child));
      }
    },
    handleCheckChange(data, checked, indeterminate) {
      // 处理节点选中状态变化的逻辑
    }
  }
};
&lt;/script&gt;
```

### 关键点说明

1. **开关组件 (`el-switch`)**：
   - 使用 `v-model` 绑定 `switchValue`，并通过 `@change` 事件触发 `switchChange` 方法。
   
2. **树组件 (`el-tree`)**：
   - 使用 `ref=&quot;treeRef&quot;` 获取树组件实例。
   - 使用 `:default-expand-all=&quot;isExpand&quot;` 控制初始展开状态。
   
3. **方法 (`switchChange`)**：
   - 切换 `isExpand` 状态，并调用 `updateTreeExpansion` 方法更新所有节点的展开状态。
   
4. **递归更新节点 (`expandNode`)**：
   - 遍历树形结构，递归设置每个节点的 `expanded` 属性为当前的 `isExpand` 状态。

## 注意事项

- **异步更新**：使用 `this.$nextTick` 确保 DOM 更新后再执行后续操作，以避免同步问题。
- **性能优化**：对于大型树结构，考虑分页或懒加载以提高性能。
- **样式调整**：根据实际需求调整 `el-switch` 的颜色和其他样式属性。





</content:encoded></item><item><title>ant-design-pro修改路径后运行报错 Can not resolve dependence ant-design-pro/node_modules/ 错误的解决方法</title><link>https://www.jason-z.com/posts/ant-design-pro-cannot-resolve-dependence-error/</link><guid isPermaLink="true">https://www.jason-z.com/posts/ant-design-pro-cannot-resolve-dependence-error/</guid><pubDate>Mon, 17 Jun 2024 18:02:45 GMT</pubDate><content:encoded>
## 概述

在使用 Ant Design Pro 开发项目时，如果修改了文件夹名称或路径，可能会遇到以下错误提示：

```
Browserslist: caniuse-lite is outdated. Please run:
 npx update-browserslist-db@latest
 Why you should do it regularly: https://github.com/browserslist/update-db#readme
event - [Webpack] Compiled in 2798 ms (521 modules)
info  - [MFSU] buildDeps since cacheDependency has changed
wait  - [Webpack] Compiling...
error - Can not resolve dependence : &apos;/Users/jasonz/Code/byteee-ids/node_modules/.pnpm/@umijs+renderer-react@4.1.1_react-dom@18.1.0_react@18.1.0/node_modules/@umijs/renderer-react&apos;, please install it
error - AssertionError [ERR_ASSERTION]: dependence not found: /Users/jasonz/Code/byteee-ids/node_modules/.pnpm/@umijs+renderer-react@4.1.1_react-dom@18.1.0_react@18.1.0/node_modules/@umijs/renderer-react
   at Dep.buildExposeContent (/Users/jasonz/Code/bids-web/node_modules/.pnpm/@umijs+mfsu@4.1.1/node_modules/@umijs/mfsu/dist/dep/dep.js:90:31)
   at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
   at async DepBuilder.writeMFFiles (/Users/jasonz/Code/bids-web/node_modules/.pnpm/@umijs+mfsu@4.1.1/node_modules/@umijs/mfsu/dist/depBuilder/depBuilder.js:159:23)
   at async DepBuilder.build (/Users/jasonz/Code/bids-web/node_modules/.pnpm/@umijs+mfsu@4.1.1/node_modules/@umijs/mfsu/dist/depBuilder/depBuilder.js:137:7)
   at async MFSU.buildDeps (/Users/jasonz/Code/bids-web/node_modules/.pnpm/@umijs+mfsu@4.1.1/node_modules/@umijs/mfsu/dist/mfsu/mfsu.js:227:7)
{
 generatedMessage: false,
 code: &apos;ERR_ASSERTION&apos;,
 actual: null,
 expected: true,
 operator: &apos;==&apos;
}
```

该错误表示依赖项解析失败，通常是因为路径更改导致缓存失效。

## 解决办法

### 清理缓存并重新启动

1. **删除 `.cache` 目录**：
   - 进入项目的 `node_modules` 目录，找到并删除 `.cache` 文件夹。
   
   ```bash
   rm -rf node_modules/.cache
   ```

2. **重新安装依赖**（可选）：
   - 如果问题仍然存在，建议重新安装所有依赖项以确保没有遗漏或损坏的包。
   
   ```bash
   npm install
   ```

3. **重新启动项目**：
   - 使用 `npm run start` 命令重新启动项目。
   
   ```bash
   npm run start
   ```

### 注意事项

- **路径一致性**：确保项目路径和配置文件中的路径保持一致，避免因路径不匹配导致的问题。
- **依赖更新**：定期更新依赖库，特别是 `caniuse-lite`，以确保兼容性和性能优化。
- **环境变量**：检查是否有任何环境变量影响了依赖项的解析。

</content:encoded></item><item><title>spring boot 3.x 根据URL前缀添加不同过滤器的解决方法</title><link>https://www.jason-z.com/posts/spring-boot-3x-add-different-filter-on-different-url-pattern/</link><guid isPermaLink="true">https://www.jason-z.com/posts/spring-boot-3x-add-different-filter-on-different-url-pattern/</guid><pubDate>Sun, 02 Jun 2024 18:44:13 GMT</pubDate><content:encoded>

## 简介

在 Spring Boot 3.x 中，根据不同的 URL 前缀添加不同的过滤器可以通过多种方式实现。本文介绍了三种方案，最终第三种方案被证明是有效的。

## 方案一：使用 `securityMatcher` 匹配

通过 `securityMatcher` 方法为不同的 URL 前缀添加过滤器：

```java
http.securityMatcher(&quot;/api/**&quot;)
    .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);

http.securityMatcher(&quot;/openapi/**&quot;)
    .addFilterBefore(openApiAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
```

这种方法在某些情况下可能无法正确工作，因为多个 `securityMatcher` 可能会相互干扰。

## 方案二：定义不同的 `SecurityFilterChain`

通过定义多个 `SecurityFilterChain` 来为不同的 URL 前缀配置安全策略和过滤器：

```java
@Bean
@Order(1)
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -&gt; csrf.disable())
        .securityMatcher(&quot;/api/**&quot;)
        .authorizeHttpRequests(authorize -&gt; authorize
            .requestMatchers(&quot;/api/auth/**&quot;, &quot;/api/user/create&quot;).permitAll()
            .anyRequest().authenticated()
        )
        .exceptionHandling(exception -&gt; exception
            .authenticationEntryPoint(authenticationEntryPoint)
        )
        .sessionManagement(session -&gt; session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

@Bean
@Order(2)
public SecurityFilterChain openApiSecurityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -&gt; csrf.disable())
        .securityMatcher(&quot;/openapi/**&quot;)
        .authorizeHttpRequests(authorize -&gt; authorize
            .requestMatchers(&quot;/openapi/**&quot;).permitAll()
            .anyRequest().authenticated()
        )
        .exceptionHandling(exception -&gt; exception
            .authenticationEntryPoint(authenticationEntryPoint)
        )
        .sessionManagement(session -&gt; session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .addFilterBefore(openApiAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
}
```

这种方法通过定义多个 `SecurityFilterChain` 来确保每个 URL 前缀有独立的安全配置，避免了冲突。

## 方案三：重写 `shouldNotFilter` 方法

通过在过滤器中重写 `shouldNotFilter` 方法来控制过滤器的应用范围：

```java
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
    String path = request.getRequestURI();
    return path.startsWith(&quot;/openapi/&quot;);
}
```

这种方式简单且有效，适用于需要针对特定路径禁用过滤器的场景。

### 最终选择

经过测试，**方案三**是最有效的解决方案。它通过简单的条件判断实现了对不同 URL 前缀的过滤器应用，避免了复杂的配置和潜在的冲突问题。




</content:encoded></item><item><title>github action 使用ssh登录rvm环境的问题</title><link>https://www.jason-z.com/posts/github-action-ssh-rvm-enviorment/</link><guid isPermaLink="true">https://www.jason-z.com/posts/github-action-ssh-rvm-enviorment/</guid><pubDate>Mon, 13 May 2024 03:43:51 GMT</pubDate><content:encoded>
## 概述

在远程主机上安装了 RVM 和相关工具（如 Node.js、npm、npx、pm2）后，通过 GitHub Actions 使用 SSH 执行命令时，可能会遇到以下错误提示：

```
npm command not found
npx command not found
pm2 command not found
```

这些错误通常是因为 SSH 会话没有加载 RVM 环境配置，导致无法找到相应的命令。本文将介绍两种解决方法。

## 解决办法

### 方法一：在 GitHub Actions 中加载 RVM 配置

可以在 GitHub Actions 的工作流文件中添加加载 RVM 配置的脚本，以确保 SSH 会话能够正确识别 RVM 管理的环境。

#### 示例代码

```yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: SSH and run commands
        uses: appleboy/ssh-action@v0.1.8
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            export NVM_DIR=&quot;$HOME/.nvm&quot;
            [ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;&amp; \. &quot;$NVM_DIR/nvm.sh&quot;
            npm --version
            npx --version
            pm2 --version
```

### 方法二：在远程主机上创建软链接

如果希望快速解决问题，可以在远程主机上为常用的命令创建软链接，使它们全局可用。不过这种方法缺乏灵活性，也失去了 RVM 的意义，因此只能作为临时解决方案。

#### 示例命令

```bash
sudo ln -s &quot;$NVM_DIR/versions/node/$(nvm version)/bin/npm&quot; &quot;/usr/local/bin/npm&quot;
sudo ln -s &quot;$NVM_DIR/versions/node/$(nvm version)/bin/pm2&quot; &quot;/usr/local/bin/pm2&quot;
sudo ln -s &quot;$NVM_DIR/versions/node/$(nvm version)/bin/npx&quot; &quot;/usr/local/bin/npx&quot;
```
</content:encoded></item><item><title>github action使用ssh登录远程主机操作步骤</title><link>https://www.jason-z.com/posts/github-action-use-ssh-key/</link><guid isPermaLink="true">https://www.jason-z.com/posts/github-action-use-ssh-key/</guid><pubDate>Mon, 13 May 2024 03:37:15 GMT</pubDate><content:encoded>
## 概述

本文将介绍如何配置 GitHub Actions 使用 SSH 登录远程主机，并执行命令。通过以下步骤，你可以确保 GitHub Actions 能够安全地与远程服务器进行交互。

## 步骤详解

### 1. 在远程主机生成私钥（如果已生成则忽略）

如果远程主机上还没有 SSH 私钥，可以通过以下命令生成：

```bash
ssh-keygen
```

这将在 `~/.ssh/` 目录下生成一对密钥文件：`id_rsa`（私钥）和 `id_rsa.pub`（公钥）。

### 2. 添加公钥到远程主机的 `authorized_keys`

将生成的公钥添加到远程主机的 `~/.ssh/authorized_keys` 文件中，以允许无密码登录：

```bash
cat ~/.ssh/id_rsa.pub &gt;&gt; ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
```

### 3. 在 GitHub 仓库中添加 Secret

将生成的私钥 (`~/.ssh/id_rsa`) 内容复制并添加到 GitHub 仓库的 Secrets 中：

1. 进入 GitHub 仓库页面。
2. 点击左侧菜单中的 **Settings**。
3. 选择 **Secrets and variables** -&gt; **Actions**。
4. 点击 **New repository secret**。
5. 将私钥内容粘贴到值字段中，并命名为 `SSH_PRIVATE_KEY`。

![添加 Secret](https://backend.jason-z.com/uploads/image_8d7b531dba.png)

### 4. 使用 `ssh-action` 组件

在 GitHub Actions 工作流文件中使用 `appleboy/ssh-action` 组件来执行远程命令。以下是一个示例工作流文件：

```yaml
name: Deploy to Remote Server

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Executing remote SSH commands using SSH key
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.PORT }}
          script: |
            whoami
            # 其他需要执行的命令
```



</content:encoded></item><item><title>nextjs如何添加ads.txt</title><link>https://www.jason-z.com/posts/nextjs-add-ads-txt/</link><guid isPermaLink="true">https://www.jason-z.com/posts/nextjs-add-ads-txt/</guid><pubDate>Mon, 13 May 2024 02:41:10 GMT</pubDate><content:encoded>

## 简介

如果你的 Next.js 网站接入了 Google AdSense 广告，但没有正确配置 `ads.txt` 文件，可能会在后台收到以下提示：

&gt; 有收益损失风险 - 您需要纠正 ads.txt 文件存在的一些问题，以免严重影响您的收入。

为了确保广告展示正常并避免收益损失，你需要正确配置 `ads.txt` 文件。

## 添加 ads.txt 文件

### 步骤

1. **创建 `ads.txt` 文件**

   在你的 Next.js 项目的 `public` 目录下创建一个名为 `ads.txt` 的文件。如果 `public` 目录不存在，请先创建该目录。

2. **编辑 `ads.txt` 文件内容**

   将以下内容复制到 `ads.txt` 文件中，并将其中的 `pub-0000000000000000` 替换为你的广告发布商 ID：

   ```plaintext
   google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
   ```

   示例：

   ```plaintext
   google.com, pub-1234567890123456, DIRECT, f08c47fec0942fa0
   ```

3. **验证配置**

   确保 `ads.txt` 文件已正确放置在 `public` 目录下，并且可以通过访问 `https://yourdomain.com/ads.txt` 查看文件内容。

## 注意事项

- **广告发布商 ID**：请务必使用你自己的广告发布商 ID，以确保广告展示和收益统计的准确性。
- **文件路径**：`ads.txt` 文件必须放在 `public` 目录下，以便能够通过域名直接访问。

更多关于 `ads.txt` 的信息可以参考 [Google 官方文档](https://support.google.com/admanager/answer/9049446)。
```



</content:encoded></item><item><title>Centos7.x升级Openssl版本</title><link>https://www.jason-z.com/posts/centos7-upgrade-openssl-version/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos7-upgrade-openssl-version/</guid><pubDate>Sun, 07 Apr 2024 14:42:40 GMT</pubDate><content:encoded>
## 概述

CentOS 7.x 默认安装的 OpenSSL 版本为 1.0.x，在实际使用中经常会需要更高版本的 OpenSSL。本文将介绍两种升级 OpenSSL 的方法：通过 `yum` 包管理器和源码编译。

## 方法一：通过 `yum` 升级

### 步骤 1：更新默认的 `yum` 源

首先尝试使用默认的 `yum` 源进行更新：

```bash
yum update openssl
```

如果发现版本没有变化，可以考虑安装 EPEL 源以获取更高版本的 OpenSSL。

### 步骤 2：安装 EPEL 源

```bash
yum install -y epel-release
```

### 步骤 3：安装 OpenSSL 1.1 版本

```bash
yum install -y openssl11 openssl11-devel
```

### 步骤 4：查找并替换高版本的 OpenSSL

查找新安装的 OpenSSL 1.1 文件路径：

```bash
whereis openssl11
```

输出示例：
```
openssl11: /usr/bin/openssl11 /usr/lib64/openssl11 /usr/include/openssl11 /usr/share/man/man1/openssl11.1.gz
```

将系统默认的 OpenSSL 替换为新版本：

```bash
mv /usr/bin/openssl /usr/bin/openssl.old
ln -s /usr/bin/openssl11 /usr/bin/openssl

mv /usr/lib64/libssl.so /usr/lib64/libssl.so.old
ln -s /usr/lib64/openssl11/libssl.so /usr/lib64/libssl.so

mv /usr/lib64/libcrypto.so /usr/lib64/libcrypto.so.old
ln -s /usr/lib64/openssl11/libcrypto.so /usr/lib64/libcrypto.so

mv /usr/lib64/openssl /usr/lib64/openssl.old
ln -s /usr/lib64/openssl11 /usr/lib64/openssl

mv /usr/include/openssl /usr/include/openssl.old
ln -s /usr/include/openssl11 /usr/include/openssl
```

验证版本是否已成功升级：

```bash
openssl version
```

输出示例：
```
OpenSSL 1.1.1k FIPS 25 Mar 2021
```

## 方法二：通过源码编译升级

### 步骤 1：下载高版本 OpenSSL

进入 `/usr/local/src` 目录并下载 OpenSSL 源码包：

```bash
cd /usr/local/src
wget --no-check-certificate https://www.openssl.org/source/openssl-1.1.1q.tar.gz
```

### 步骤 2：解压并编译

```bash
tar zxvf openssl-1.1.1q.tar.gz
cd openssl-1.1.1q
./config -fPIC shared zlib --prefix=/usr/local/openssl
make
make install
```

### 步骤 3：配置动态链接库路径

将新安装的 OpenSSL 库路径添加到动态链接库配置文件中：

```bash
echo &quot;/usr/local/openssl/lib&quot; &gt;&gt; /etc/ld.so.conf
ldconfig
```

### 步骤 4：替换系统默认的 OpenSSL

```bash
mv /usr/bin/openssl /usr/bin/openssl.old
ln -s /usr/local/openssl/bin/openssl /usr/bin/openssl

ln -s /usr/local/openssl/include/openssl /usr/include/openssl

echo &quot;/usr/local/openssl/lib&quot; &gt;&gt; /etc/ld.so.conf
ldconfig -v
```

验证版本是否已成功升级：

```bash
openssl version
```

输出示例：
```
OpenSSL 1.1.1q 5 Jul 2022
```

## 注意事项

- **备份**：在替换系统默认的 OpenSSL 文件之前，建议先备份原有文件（如 `openssl.old`），以防出现问题时可以恢复。
- **依赖关系**：确保其他依赖于 OpenSSL 的软件和服务在升级后仍能正常运行，必要时重新编译或更新这些依赖项。
- **测试环境**：建议先在测试环境中进行升级操作，确认无误后再应用到生产环境。
</content:encoded></item><item><title>radiusclient.conf No such file or directory 错误的解决办法</title><link>https://www.jason-z.com/posts/radiusclient-conf-no-such-file-or-directory-error-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/radiusclient-conf-no-such-file-or-directory-error-solution/</guid><pubDate>Tue, 12 Mar 2024 13:38:17 GMT</pubDate><content:encoded>

## 简介

在 Ubuntu 上安装 Asterisk 时，可能会遇到以下错误提示：

```
radcli: rc_read_config: rc_read_config: can&apos;t open /etc/radiusclient-ng/radiusclient.conf: No such file or directory
```

这通常是因为配置文件路径不正确或文件不存在。以下是解决该问题的方法。

## 解决办法

### 修改配置文件路径

通过修改 Asterisk 的配置文件来更正 `radiusclient.conf` 的路径。具体步骤如下：

1. **编辑 `cdr.conf` 文件**

   使用 `sed` 命令批量替换配置项：

   ```bash
   sudo sed -i &apos;s/^;\[radius\]/\[radius\]/g&apos; /etc/asterisk/cdr.conf
   sudo sed -i &apos;s/^;radiuscfg =&gt; \/usr\/local\/etc\/radiusclient-ng\/radiusclient.conf/radiuscfg =&gt; \/etc\/radcli\/radiusclient.conf/g&apos; /etc/asterisk/cdr.conf
   ```

2. **编辑 `cel.conf` 文件**

   同样使用 `sed` 命令批量替换配置项：

   ```bash
   sudo sed -i &apos;s/^;radiuscfg =&gt; \/usr\/local\/etc\/radiusclient-ng\/radiusclient.conf/radiuscfg =&gt; \/etc\/radcli\/radiusclient.conf/g&apos; /etc/asterisk/cel.conf
   ```

### 验证配置

确保 `/etc/radcli/radiusclient.conf` 文件存在且内容正确。如果文件不存在，请根据需要创建或复制正确的配置文件。


</content:encoded></item><item><title>java 错误 不支持发行版本 5 错误的解决办法</title><link>https://www.jason-z.com/posts/java-unsupport-build-version-5-error-soliution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/java-unsupport-build-version-5-error-soliution/</guid><pubDate>Mon, 11 Mar 2024 03:03:01 GMT</pubDate><content:encoded>
## 概述

在使用 IntelliJ IDEA 打开项目时，可能会遇到以下错误提示：

```
java: 错误: 不支持发行版本 5
```

该错误通常表示项目的编译器设置或 JDK 版本配置不正确。本文将介绍如何解决这一问题。

## 解决办法

### 方法一：统一模块的 JDK 版本

1. **打开项目设置**：
   - 在 IntelliJ IDEA 中，点击菜单栏中的 `File` -&gt; `Settings`（Windows/Linux）或 `IntelliJ IDEA` -&gt; `Preferences`（macOS）。

2. **查找 Java 编译器设置**：
   - 在左侧导航栏中选择 `Build, Execution, Deployment` -&gt; `Compiler` -&gt; `Java Compiler`。

3. **检查模块的 JDK 版本**：
   - 确认所有模块的 JDK 版本是否一致。如果使用的 JDK 版本是 17，则应将所有模块的 JDK 版本统一修改为 17。

   ![设置模块 JDK 版本](https://backend.jason-z.com/uploads/image_e8fc0825be.png)

4. **应用更改并重新构建项目**：
   - 应用更改后，重新构建项目以确保设置生效。

### 方法二：调整项目字节码版本

如果上述方法未能解决问题，可以尝试调整项目的字节码版本：

1. **打开项目结构设置**：
   - 在 IntelliJ IDEA 中，点击菜单栏中的 `File` -&gt; `Project Structure`。

2. **设置项目字节码版本**：
   - 在左侧导航栏中选择 `Project`，然后将 `Project bytecode version` 设置为 17。

   ![设置项目字节码版本](https://backend.jason-z.com/uploads/image_e8fc0825be.png)

3. **应用更改并重新构建项目**：
   - 应用更改后，重新构建项目以确保设置生效。

</content:encoded></item><item><title>cordova提示No usable Android build tools found错误的解决方案</title><link>https://www.jason-z.com/posts/cordova-no-usable-android-build-tools-found-error-solutions/</link><guid isPermaLink="true">https://www.jason-z.com/posts/cordova-no-usable-android-build-tools-found-error-solutions/</guid><pubDate>Wed, 03 Jan 2024 07:27:31 GMT</pubDate><content:encoded>
该错误表示系统中没有找到可用的 Android Build Tools 版本，或者已安装的版本不符合要求。本文将介绍如何解决这一问题。

## 解决办法

### 方法一：通过 Android Studio SDK Manager 安装所需的 Build Tools

1. **打开 Android Studio**：
   - 启动 Android Studio 并进入主界面。

2. **打开 SDK Manager**：
   - 点击菜单栏中的 `Tools` -&gt; `SDK Manager`，或使用快捷键 `Ctrl + Alt + S` 打开设置，然后选择 `Appearance &amp; Behavior` -&gt; `System Settings` -&gt; `Android SDK`。

3. **安装所需的 Build Tools 版本**：
   - 在 `SDK Tools` 标签下，勾选需要的 Android Build Tools 版本（如 33.0.2），然后点击 `Apply` 进行安装。

4. **验证安装**：
   - 安装完成后，确保新版本的 Build Tools 已正确安装并可用。

### 方法二：通过命令行安装 Build Tools

如果你更喜欢使用命令行工具，可以通过 `sdkmanager` 命令来安装所需的 Build Tools 版本。

1. **安装 `sdkmanager`**（如果尚未安装）：
   - 确保你已经安装了 Android SDK Command-line Tools。如果没有，请先安装。

2. **安装特定版本的 Build Tools**：
   - 使用以下命令安装所需的 Build Tools 版本（例如 33.0.2）：

     ```bash
     sdkmanager &quot;build-tools;33.0.2&quot;
     ```

3. **更新环境变量**（如果需要）：
   - 确保 `sdkmanager` 和 `build-tools` 路径已添加到系统的环境变量中。

### 注意事项

- **版本一致性**：确保安装的 Build Tools 版本与项目需求一致，避免因版本不匹配导致的问题。
- **依赖项检查**：安装 Build Tools 后，建议重新构建项目以确保所有依赖项都已正确解析。
- **环境配置**：确保 `ANDROID_HOME` 和 `PATH` 环境变量已正确配置，指向正确的 Android SDK 和 Build Tools 目录。
</content:encoded></item><item><title>java swing如何选择文件夹</title><link>https://www.jason-z.com/posts/java-swing-select-directory/</link><guid isPermaLink="true">https://www.jason-z.com/posts/java-swing-select-directory/</guid><pubDate>Fri, 22 Dec 2023 09:40:31 GMT</pubDate><content:encoded>
## 概述

本文将介绍如何使用 Java Swing 实现文件夹选择功能。通过 `JFileChooser` 组件，可以轻松地让用户选择文件夹，并获取所选文件夹的路径。

## 示例代码

### 完整示例代码

```java
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;

public class DemoJFileChooser extends JPanel implements ActionListener {
    private JButton go;
    private JFileChooser chooser;
    private String choosertitle;

    public DemoJFileChooser() {
        go = new JButton(&quot;选择文件夹&quot;);
        go.addActionListener(this);
        add(go);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        chooser = new JFileChooser();
        chooser.setCurrentDirectory(new java.io.File(&quot;.&quot;));
        chooser.setDialogTitle(choosertitle);
        chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);

        // 禁用“所有文件”选项
        chooser.setAcceptAllFileFilterUsed(false);

        if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
            System.out.println(&quot;当前目录: &quot; + chooser.getCurrentDirectory());
            System.out.println(&quot;选择的文件夹: &quot; + chooser.getSelectedFile());
        } else {
            System.out.println(&quot;未选择文件夹&quot;);
        }
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(200, 200);
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame(&quot;选择文件夹示例&quot;);
        DemoJFileChooser panel = new DemoJFileChooser();

        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });

        frame.getContentPane().add(panel, &quot;Center&quot;);
        frame.setSize(panel.getPreferredSize());
        frame.setVisible(true);
    }
}
```

### 代码说明

1. **导入必要的包**：
   - 导入 `javax.swing.*` 和 `java.awt.event.*` 包，用于创建图形用户界面和处理事件。
   
2. **创建 `DemoJFileChooser` 类**：
   - 继承自 `JPanel` 并实现 `ActionListener` 接口，以便处理按钮点击事件。

3. **初始化组件**：
   - 创建一个按钮 `go`，并为其添加点击事件监听器。
   - 在构造函数中初始化按钮并将其添加到面板中。

4. **处理按钮点击事件**：
   - 创建 `JFileChooser` 实例，设置其初始目录、对话框标题和文件选择模式为仅选择文件夹。
   - 禁用“所有文件”选项。
   - 显示文件选择对话框，并根据用户选择输出相应的信息。

5. **主方法**：
   - 创建并配置 `JFrame`，添加 `DemoJFileChooser` 面板，并设置窗口关闭操作。

</content:encoded></item><item><title>ktor上传过大文件导致OOM错误的解决办法</title><link>https://www.jason-z.com/posts/ktor-upload-big-file-oom-error-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/ktor-upload-big-file-oom-error-solution/</guid><pubDate>Tue, 05 Dec 2023 00:56:33 GMT</pubDate><content:encoded>
## 概述

在使用 Ktor 上传大文件时，可能会遇到以下错误提示：

```
Android: OOM or &quot;Broken delimiter occurred&quot; error when making multipart request with a file
```

这些错误通常是由于内存不足（OOM）或文件处理不当引起的。本文将介绍如何解决这一问题。

## 解决办法

### 原代码

原代码在处理文件上传时，将整个文件读取到内存中，导致内存不足：

```kotlin
val fileBytes = part.streamProvider().readBytes()
file.writeBytes(fileBytes)
```

### 新代码

为了避免内存不足的问题，可以使用流式处理，逐块读取和写入文件：

```kotlin
part.streamProvider().use { input -&gt;
    file.outputStream().buffered().use { output -&gt;
        input.copyTo(output)
    }
}
```

### 详细步骤

1. **使用 `use` 语句管理资源**：
   - `use` 语句确保输入流和输出流在使用完毕后自动关闭，避免资源泄漏。

2. **逐块读取和写入文件**：
   - `input.copyTo(output)` 方法逐块读取输入流并写入输出流，避免一次性加载整个文件到内存中。

### 注意事项

- **文件大小限制**：根据服务器和客户端的配置，合理设置文件大小限制，防止上传过大的文件。
- **性能优化**：确保服务器有足够的内存和带宽来处理文件上传。
- **错误处理**：添加适当的错误处理逻辑，以便在上传过程中捕获和处理异常。

</content:encoded></item><item><title>小张买房记</title><link>https://www.jason-z.com/posts/xiao-zhang-house-story/</link><guid isPermaLink="true">https://www.jason-z.com/posts/xiao-zhang-house-story/</guid><pubDate>Sun, 12 Nov 2023 06:26:00 GMT</pubDate><content:encoded>



小张是一个来普通农村家庭的孩子，大学毕业之后，他来到了西子湖畔，虽然他毕业的学校不算太差，但是那时仍然受08金融危机的影响，大学生就业并不是很理想，小张也是经过磕磕碰碰才勉强找到了一份工作。

小张那个时候，虽然和女朋友蜗居在10来平方的郊区出租房里，每天上下班都要2,3个小时的通勤，但望着这个闪耀着霓虹灯的城市，小张还是有充满一股年轻人的热情的干劲，他觉得他能适应着这座城市。


经过2，,3年的努力，小张慢慢的从一家公司到了另外一家的公司，待遇水平也有了一定的提升，在新的公司里与同事打交道，每天听到最多的就是两件事情就是：某某同事准备去买哪里的房子，亦或者某某拆二代又收了多少租，谈起这些话题，小张想着自己个位数的存款，也只可能敬而远之。


虽然小张没有房，但是他的女朋友还是没有嫌弃他，他们步入了婚姻的殿堂并且生下了可爱的宝宝，为了解决小张，小张老婆，以及小张妈妈，小张的孩子蜗居在一个房间里的尴尬，小张决定咬咬牙，换到了两居室的出租房。

孩子和生活压力，让小张放弃了去大公司进一步提升的职业方向，他选择了加入了兄弟的创业团队，或许他也在赌，有一天他们创业的项目会让他们都一夜&quot;暴富&quot;，至于买房就更不在话下了。因此，他决定全身心地投入，没日没夜地加班干，就是希望公司的创业项目能够早日成功。


就这样，又过去了几年，中间又不记得搬了几次家，小张的公司依旧还在，只是梦想却变得遥远起来，杭州这座城市也在几年之间发生了巨大的变化，几年前都还是荒草地的郊区地方，如今全是高楼林立，房价更是翻了几番。。


小张的小孩，在大城市里上学的，幼儿园的时候基本上还是去根据自己的情况去选择来上，然而到了准备小学之后，住房，户口却成为了硬性条件，作为“无等生”的家庭，他们要不只能去所谓的“农民工”学校，要不就去选择高达十来万一年的私立。

小张的经济情况并不允许他这么做。于是，小张和他的老婆，做出了一个重大的决定：离开。

他们并没有回到自己的老家，而是选择了一个距离杭州2000公里之外的小城市：白娘子的老家。

来到了一个这么远和相对陌生的城市，加之小孩要入学，因此家里人都建议直接选择在当地买房，以满足入学条件，于是，在一个周末，小张从杭州飞了过去，在没做任何买房的功课下，小张去了买房的现场。

那是一个很漂亮的卖房建筑，门口停着很多的大巴车，拉着从全国各地来买房的人，大厅内，人声鼎沸，售房销售们，一个个忙的热火朝天，甚至根本没有接待小张的时间，小张刚开始选择了几个房号，转头就告知被别人抢了，让他们抓紧时间决定。

于是，就这样小张就简单看了一下样板房，卷的还不错，就下了订金，小张人生第一次买房，就这样稀里糊涂的定下了，整个过程可能不到半个小时。

小张的杭州的工作交接完成之后，就举家迁到了都江堰这座城市，并且选择了离交新房很近的小区租房住了下来，

虽然选择的地方并不在市中心，另外由于是新的楼盘区域，人气也不旺，但是小张想着自己每天在接送的孩子的途中都能目睹着自己的房子一层层慢慢建起来，那种感觉还是很不错的。


两年的时间其实还是挺快的，这期间大家都忙着抗疫，新冠病毒的到来，虽然让小张的停工了几个月，但是大疫之后，工人们又陆陆续续来了，甚至当小张那天看到了“封顶”的告示，总感觉自己第二年就可以顺利入驻进去了。


然而，这种好日子并没有持久，先是传闻了恒大楼盘暴雷，一时间关于房地产的话题一跃成为大家讨论的议题，小张虽然并没有太多关心，但总想自己不是恒大的楼盘应该不会牵连其中，然后事实就很快打脸，小张的楼盘的工人逐渐变少了，到了新年之后的一个月，更是迟迟没有一个工人来建设。


小张知道，出事儿了。

于是小张，各方打听，终于找到了同样买这个楼盘的业主群，大家在群里议论纷纷，争执不休，很多业主都只是在群里表达自己的想法，但是却么有几个愿意行动。

小张决定当这个“出头鸟”，组织了业主进行了关于小区交房问题的WQ，这是第一次小张生平第一次参加WQ。平时不善言辞的小张，为了交房还是鼓起了勇气和开发商针锋相对，为小区业主争取到了小区可以复工的答复。


然而小张终究还是太年轻，业主的真诚只是换来了开发商的套路，虽然合同交房日期的临近，越来越多购房者更加关心了这个问题，业主群从几十人​到达了上百人，在面临开发生一次次拖延和无实际意义的答复之后，业主们一直决定为了交房将会付诸更大的行动，让政府能为老百姓解决问题。

那是小张第一次参加那样的活动，数百人围在门口，几十名武警团团围住，业主们顶着烈日跪着，齐声高喊着，情绪激动的业主哭着，躺着，甚至于jc发生了​肢体上的冲突，那个场面让小张永生难忘。

经过业主们每次努力，虽然争取到了更大的领导，开发商的项目经理，项目总，区域总，政府住建的局长，市长，业主都见到了，谈了，然而最终的结果却都是基本一致​，小张的房子进展确实迟迟进展很慢。

终于有一天，之前都不愿意待见业主的局长和项目总，主动联系到了业主，告知他们为业主争取到了一笔资金，起初大家还不信，后来在多方证实下，确有此事，国家要​保交楼了。


虽然，有了钱，然后并不代表能修好房子，楼市所产生的的连锁反应依旧会阻碍着保交楼的进行，这一期间，小张和他们的业主，为此又参加不知道多少次的WQ，以及与工人，开发商，zf部门等等​的多方博弈。


一年之后，小张的房子总算是“完工”了，虽然他的小区无论是绿化和硬件配置上都根本不配他当时的房价，房屋也是各种问题，但总算达到了开发商所谓的交房条件，他们完成了所谓的保交楼，虽然小张和业主们都心有不甘，然而在如此环境之下不得不选择妥协。

今天是双十一，当人们还沉浸在网购的兴奋之中，小张的房子装修也基本告一段落了，明天他就要搬入他人生的第一套新房里了，虽然都年近40了才有了房子，说出来确实给广大年轻人丢脸了，但是总算是结束了十几年的租房生涯了。


没有了当年对新房的期待和兴奋，但小张还是非常开心等到了这一天的到来，然而他想起了遥远的父亲却永远没法见证这一时刻，心里不免有些痛楚。

小张的房子视野不错，旁边和更远的地方，伫立着许许多多锈迹斑斑的塔吊和空楼，今夜的雨声滴滴哒哒下个不停，似乎在诉述着他们​未来主人的故事。。。

​
</content:encoded></item><item><title>Android打包提示Missing classes detected while running R8 错误解决方案</title><link>https://www.jason-z.com/posts/android-package-missing-classes-detected-while-running-r8-error-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/android-package-missing-classes-detected-while-running-r8-error-solution/</guid><pubDate>Wed, 08 Nov 2023 14:55:29 GMT</pubDate><content:encoded>
## 概述

在使用 R8 进行代码混淆和压缩时，有时会遇到以下错误提示：

```
Missing classes detected while running R8. Please add the missing classes or apply additional keep rules that are generated in your path to missing_rules.txt.
```

该错误表示 R8 在处理代码时检测到某些类缺失，导致混淆和压缩失败。本文将介绍如何解决这一问题。

## 解决办法

### 步骤一：查找生成的 `missing_rules.txt` 文件

1. **定位文件路径**：
   - 在项目目录下找到 `app/build/outputs/mapping/release/missing_rules.txt` 文件。
   
2. **查看文件内容**：
   - 该文件中包含了类似以下的规则建议：

     ```proguard
     -dontwarn android.os.ServiceManager
     -dontwarn com.bun.miitmdid.core.MdidSdkHelper
     -dontwarn com.bun.miitmdid.interfaces.IdSupplier
     -dontwarn com.google.firebase.iid.FirebaseInstanceId
     -dontwarn com.google.firebase.iid.InstanceIdResult
     -dontwarn com.tencent.android.tpush.otherpush.OtherPushClient
     ```

### 步骤二：将规则添加到 `proguard-rules.pro`

1. **打开 `proguard-rules.pro` 文件**：
   - 通常位于项目的 `app` 目录下。

2. **复制并粘贴规则**：
   - 将 `missing_rules.txt` 中的规则复制到 `proguard-rules.pro` 文件中。

3. **保存文件**：
   - 确保所有修改已保存。

### 示例

假设 `missing_rules.txt` 文件内容如下：

```proguard
-dontwarn android.os.ServiceManager
-dontwarn com.bun.miitmdid.core.MdidSdkHelper
-dontwarn com.bun.miitmdid.interfaces.IdSupplier
-dontwarn com.google.firebase.iid.FirebaseInstanceId
-dontwarn com.google.firebase.iid.InstanceIdResult
-dontwarn com.tencent.android.tpush.otherpush.OtherPushClient
```

你需要将这些规则添加到 `proguard-rules.pro` 文件中：

```proguard
# proguard-rules.pro

# 忽略特定类的警告
-dontwarn android.os.ServiceManager
-dontwarn com.bun.miitmdid.core.MdidSdkHelper
-dontwarn com.bun.miitmdid.interfaces.IdSupplier
-dontwarn com.google.firebase.iid.FirebaseInstanceId
-dontwarn com.google.firebase.iid.InstanceIdResult
-dontwarn com.tencent.android.tpush.otherpush.OtherPushClient
```

### 注意事项

- **保持一致性**：确保 `proguard-rules.pro` 文件中的规则与 `missing_rules.txt` 中的一致。
- **测试构建**：添加规则后，重新构建项目以验证问题是否解决。
- **依赖库更新**：如果问题依然存在，检查是否有依赖库需要更新或配置调整。


</content:encoded></item><item><title>中国dota玩家的进化</title><link>https://www.jason-z.com/posts/china-dota-player-evolution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/china-dota-player-evolution/</guid><pubDate>Mon, 30 Oct 2023 06:26:00 GMT</pubDate><content:encoded>

今年的TI12在凌晨结束了，本来想着看看是不是能不能熬一熬去看看比赛的，结果身体实在熬不住，就早早睡了。早上一起床，就赶紧打开手机，去看了一下结果，心情先是失落了一下，然而这种感觉，却只停留了几秒，转眼就没了。

也许其他dota玩家们一样，虽然内心终有遗憾，但是这个结果确实很满意的。因为只有dota玩家知道，近些年来中国dota经历着什么。

从dota1时代，中国dota1统治时间的辉煌，dota2偶数年夺冠的期待，TI6 Wings战队创造的奇迹，再到TI8的折戟沉沙，以后Ti10之后的中国dota的落寞之势。
 

2023年，中国dota更是萎靡不振，各项大赛甚至连个像样的奖项都没有,本届TI更是历史性只有2个名额（以前至少是4个），这两个队伍赛前都不被人看好，一个是TI临近时几个退役选手组成的“主播队”，另外一个是“全村的希望”LGD，然而今年的状态很是不好。

然而，结果是出乎大家的意料的，唯一进入ti比赛的两只中国队伍，虽然没能摘得桂冠，但是却双双进入四强。面对今年状态非常好的GG战队以及状态几乎“变态”的TS战队，最终只能无奈打出“技不如人，甘拜下风”。

当然，让我今年深有感慨的并不仅仅是中国战队的成绩，而是dota玩家的进化。众所周知，游戏环境的舆论环境是很恶劣的，dota也不例外，在这里面充斥着各种各样的人以及各种各样的言论，他们站在上帝视角，用接近完美理论的病态主义去指导和要求赛场上的选手，当然现实中他们的分数可能只有几百分，甚至根本不了解这个游戏，他们有一个统一的外号：喷子。

喷子的世界无处不在，在社交媒体如此发达的社会里，他们无孔不入，无所不用其极，用来发泄自己现实生活的不公，而职业玩家和选手也是人，过多的负面情绪，只会让他失去对原本事务的专注，承受更多的心理压力，最终被影响而提早结束职业生涯的人比比皆是。这甚至让我想到了，一位曾经创造过田径历史奇迹的黄种人遭受过的同样遭遇。

是人都会犯错，更何况在瞬息万变的游戏之中，我并不是为他们的错误，   去找借口，但我们可以选择用更好的方式来表达。
 

而今年，当我再次回来观看dota比赛的时候，我似乎感觉到了一些不同的声音，甚至在中国队伍发挥不好的弹幕里也能看到有一些鼓励的话，大家不再以非要以中国队必须夺冠才是中国dota的胜利作为标准要求自己，而是更专注的去享受每一次比赛。
 

虽然，我不知道为何观众会发生这种转变，或许是人们的期望在降低，亦或者是随着年龄的成长心智愈加成熟。但是，我感觉到这种观众的转变似乎也在同样潜移默化地影响选手状态的转变：​他们变的更加自信，更加有毅力，更加有决心，从亚运会到ti的表现就足以证明了这一切。
 

或许，dota终究会成为一个dead game，但是只有有中国队去比赛，中国玩家支持他们，中国的队伍始终会在世界的舞台上闪耀光芒。





</content:encoded></item><item><title>Jetpack Compose segmented control组件</title><link>https://www.jason-z.com/posts/jetpack-compose-segementd-control-compoent/</link><guid isPermaLink="true">https://www.jason-z.com/posts/jetpack-compose-segementd-control-compoent/</guid><pubDate>Mon, 30 Oct 2023 02:40:31 GMT</pubDate><content:encoded>
## 概述

Jetpack Compose 默认并没有提供 `Segmented Control` 组件，因此我们可以自定义一个。本文将介绍如何创建一个功能齐全的 `SegmentedControl` 组件，并提供使用示例。

## 自定义 SegmentedControl 组件

### 示例代码

```kotlin
@Composable
fun SegmentedControl(
    items: List&lt;String&gt;,
    defaultSelectedItemIndex: Int = 0,
    useFixedWidth: Boolean = false,
    itemWidth: Dp = 120.dp,
    cornerRadius: Int = 10,
    @ColorRes color: Int = R.color.teal_200,
    onItemSelection: (selectedItemIndex: Int) -&gt; Unit
) {
    val selectedIndex = remember { mutableStateOf(defaultSelectedItemIndex) }
    
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 8.dp)
    ) {
        items.forEachIndexed { index, item -&gt;
            OutlinedButton(
                modifier = when {
                    useFixedWidth -&gt; Modifier
                        .width(itemWidth)
                        .zIndex(if (selectedIndex.value == index) 1f else 0f)
                    else -&gt; Modifier
                        .wrapContentSize()
                        .zIndex(if (selectedIndex.value == index) 1f else 0f)
                },
                onClick = {
                    selectedIndex.value = index
                    onItemSelection(selectedIndex.value)
                },
                shape = when (index) {
                    0 -&gt; RoundedCornerShape(
                        topStart = cornerRadius.dp,
                        bottomStart = cornerRadius.dp
                    )
                    items.size - 1 -&gt; RoundedCornerShape(
                        topEnd = cornerRadius.dp,
                        bottomEnd = cornerRadius.dp
                    )
                    else -&gt; RoundedCornerShape(0.dp)
                },
                border = BorderStroke(
                    1.dp, 
                    if (selectedIndex.value == index) {
                        colorResource(id = color)
                    } else {
                        colorResource(id = color).copy(alpha = 0.75f)
                    }
                ),
                colors = if (selectedIndex.value == index) {
                    ButtonDefaults.outlinedButtonColors(
                        backgroundColor = colorResource(id = color),
                        contentColor = Color.White
                    )
                } else {
                    ButtonDefaults.outlinedButtonColors(
                        backgroundColor = Color.Transparent,
                        contentColor = colorResource(id = color).copy(alpha = 0.9f)
                    )
                },
            ) {
                Text(
                    text = item,
                    fontWeight = FontWeight.Normal,
                    color = if (selectedIndex.value == index) {
                        Color.White
                    } else {
                        colorResource(id = color).copy(alpha = 0.9f)
                    }
                )
            }
        }
    }
}
```

### 使用示例

```kotlin
val genders = listOf(&quot;Male&quot;, &quot;Female&quot;)

SegmentedControl(
    items = genders,
    defaultSelectedItemIndex = 0
) { selectedIndex -&gt;
    Log.e(&quot;CustomToggle&quot;, &quot;Selected item: ${genders[selectedIndex]}&quot;)
}
```

### 参数说明

- **items**: 要显示在 `SegmentedControl` 中的项目列表。
- **defaultSelectedItemIndex**: 默认选中的项目索引，默认为 `0`。
- **useFixedWidth**: 是否使用固定宽度，默认为 `false`。
- **itemWidth**: 每个项目的宽度，默认为 `120.dp`。
- **cornerRadius**: 圆角半径，默认为 `10`。
- **color**: 颜色资源 ID，默认为 `R.color.teal_200`。
- **onItemSelection**: 当选择项发生变化时的回调函数。
</content:encoded></item><item><title>Android提示ContentResolver：column ‘_data‘ does not exist.错误的解决方法</title><link>https://www.jason-z.com/posts/android-contentresolver-column-data-dose-not-exist-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/android-contentresolver-column-data-dose-not-exist-solution/</guid><pubDate>Mon, 30 Oct 2023 02:27:36 GMT</pubDate><content:encoded>
## 概述

在 Android 开发中，使用 `ContentResolver` 通过 URI 获取文件路径时，可能会遇到以下错误：

```
java.lang.IllegalArgumentException: column &apos;_data&apos; does not exist
```

主要原因是从 API 29 开始，`MediaStore.Files.FileColumns.DATA` 字段已经被废弃。因此，直接通过 `_data` 列获取文件路径的方法不再适用。

## 原因分析

从 Android 10（API 级别 29）开始，Google 强制执行了更严格的存储权限策略，`MediaStore` 中的 `_data` 列被废弃，取而代之的是推荐使用 `FileDescriptor` 来读取文件内容。

## 旧版代码

之前我们使用的通过 URI 获取文件路径的代码如下：

```java
private void getContentResolverInfo(Uri uri, int width, int height, SlideFactory.MediaType mediaType) {
    Cursor cursor = null;
    long start = System.currentTimeMillis();
    try {
        cursor = context.getContentResolver().query(uri, null, null, null, null);
        if (cursor != null &amp;&amp; cursor.moveToFirst()) {
            String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
            long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
            long filePath = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
            String mimeType = context.getContentResolver().getType(uri);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (cursor != null)
            cursor.close();
    }
}
```

## 修改后的代码

为了解决上述问题，修改后的代码如下：

```java
private void getContentResolverInfo(Uri uri, int width, int height, SlideFactory.MediaType mediaType) {
    Cursor cursor = null;
    long start = System.currentTimeMillis();
    try {
        cursor = context.getContentResolver().query(uri, null, null, null, null);
        if (cursor != null &amp;&amp; cursor.moveToFirst()) {
            String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
            long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));

            // 使用 FileDescriptor 读取文件内容
            ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, &quot;r&quot;);
            if (parcelFileDescriptor != null) {
                FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
                String keystoreContent = readFileContent(fileDescriptor);
                FileInputStream fileInputStream = new FileInputStream(fileDescriptor);
                String mimeType = context.getContentResolver().getType(uri);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (cursor != null)
            cursor.close();
    }
}

public static String readFileContent(FileDescriptor fileDescriptor) {
    Log.i(TAG, &quot;readFileContent(), fileDescriptor=&quot; + fileDescriptor);
    if (fileDescriptor == null) {
        return null;
    }

    StringBuilder sb = new StringBuilder();
    try (FileInputStream fileInputStream = new FileInputStream(fileDescriptor);
         InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
         BufferedReader reader = new BufferedReader(inputStreamReader)) {

        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line).append(&quot;\n&quot;);
        }
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }

    return sb.toString();
}
```

### 关键改进点

1. **移除对 `_data` 列的依赖**：直接读取文件内容而不依赖 `_data` 列。
2. **使用 `ParcelFileDescriptor` 和 `FileDescriptor`**：通过 `FileDescriptor` 读取文件内容，确保兼容新版本的 Android。
3. **资源管理**：使用 try-with-resources 语法确保流正确关闭，避免资源泄漏。

</content:encoded></item><item><title>Android开发之忽略电池优化权限</title><link>https://www.jason-z.com/posts/android-develop-for-ignore-battery-optimizations/</link><guid isPermaLink="true">https://www.jason-z.com/posts/android-develop-for-ignore-battery-optimizations/</guid><pubDate>Mon, 30 Oct 2023 02:08:31 GMT</pubDate><content:encoded>
## 概述

在某些情况下，应用程序需要请求用户忽略电池优化以确保其后台服务能够正常运行。本文将介绍如何在 Android 应用中实现这一功能。

## 配置权限

首先，在 `AndroidManifest.xml` 文件中添加必要的权限声明：

```xml
&lt;uses-permission android:name=&quot;android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS&quot;/&gt;
```

## 实现代码

接下来，在代码中实现忽略电池优化的功能。以下是一个完整的示例方法：

### 忽略电池优化

```java
/**
 * 请求用户忽略电池优化
 */
private void ignoreBatteryOptimization(Activity activity) {
    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.M) {
        PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);

        // 判断当前应用是否已加入电池优化的白名单
        boolean hasIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName());

        if (!hasIgnored) {
            // 如果没有加入白名单，则弹出加入电池优化白名单的设置对话框
            Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
            intent.setData(Uri.parse(&quot;package:&quot; + activity.getPackageName()));

            // 确保有可用的 Activity 来处理该 Intent
            if (intent.resolveActivity(getPackageManager()) != null) {
                startActivity(intent);
            }
        } else {
            Log.d(&quot;ignoreBattery&quot;, &quot;Battery optimization already ignored.&quot;);
        }
    }
}
```

### 注意事项

- **权限声明**：确保在 `AndroidManifest.xml` 中正确声明了 `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` 权限。
- **API 版本检查**：此功能仅适用于 API 23（Android 6.0）及以上版本。
- **用户体验**：考虑到用户体验，建议仅在必要时提示用户忽略电池优化，并提供明确的说明，告知用户为什么需要这样做。

</content:encoded></item><item><title>老爸的挚友走了</title><link>https://www.jason-z.com/posts/goodbye-dads-friend/</link><guid isPermaLink="true">https://www.jason-z.com/posts/goodbye-dads-friend/</guid><pubDate>Sat, 28 Oct 2023 06:26:00 GMT</pubDate><content:encoded>


因为并不在老家，所以也没有人告知，只是昨日无意间刷到老乡的抖音，看到有人发视频在举办葬礼，才得知了这个消息。其实，前一段时间，我看到他老婆发的抖音，我就预感情况不是太好，只是没想到​会来得这么快。

老爸的挚友是一位抗击病魔的战士，

我记得2，3年前，我老爸曾经和我说过：你BF叔有一天看起来样子很糟糕，跑到我这里跟我说，大哥，兄弟我可能是我们弟兄几个走的最早的那个了，今天去医院检查，确诊是XX病，医生说我活不过过年了（两三个月的时间）... 而老爸​ 只是还他说他了一句： “没球事儿。。”
后来也不知道是身边人劝他，还是他自己想开了，想着时间不多了，还不如就再潇洒潇洒，于是和我们大队里几个退休的老家伙们一起，每天到处游山玩水，拍照发圈...
结果，没想到，他竟然“平安”地度过了那个春节，春节之后，他又去医院复查，医生说他的情况得到了很大的改善，让他继续坚持治疗，于是他一边吃靶向药，另外一面，继续保持乐观心态，每天坚持走个2万步，于是又过了一个祥和的春节，而且他的面色也红润起来，人也吃胖了，周围人都说​他看不出来像是一个得了绝症的人。

去年，年底的我老爸也检查出来和他一样的病，我老爸也第一时间联系了他，他也一直鼓励着我老爸，说你看老弟我这不也挺过了2,3年了，说我老爸身体素质好，更没问题，让我老爸放宽心点儿。并且每天都会在微信上发消息鼓励我老爸，其实和他沟通之前，我老爸其实就想放弃的，结果听了BF叔的话，他又开始接收了治疗，

然而，事与愿违，我老爸的思想包袱太重，加之疫情等因素的影响，最终没能挺过这个春节​...
春节之后，我也很少关心其他人的事情，直到后面，才注意到，之前BF叔和老婆 几乎定期拍照发圈的他们，在年后却几乎很少再看到，而最近他老婆发的那段话，让我知道，他的情况​变得不容乐观了。
纵观，我身边得了这个病的人，很少能有熬过半年了，BF叔的事迹确实算是一个奇迹了，当然，坦白来说，我觉得现在的结果对他也是一种解脱，因为我有无比深刻的体会，这个病魔对人身体和意志力的摧残​的恐怖和煎熬。
BF叔是我们大队的村医，尽管也许村里有人对他褒贬不一，但他始终是我一位尊敬的​人。
在我的记忆里，我都不会忘记，我们的一大家子人里，无论是感冒和发烧，还是其他输液治疗，BF叔总是能在老爸一个电话下，无论刮风下雨，还是深夜几点，都能够第一时间到达，给予他​能力水平下最好的治疗。
而相比老爸有些所谓称兄道弟，却在背后栽赃陷害的“哥们”，BF叔对得起他兄弟的称号。
BF叔，希望你与老爸九泉之下早日相聚，兄弟之间再把酒言欢​。

​愿世间再无病魔，​一路走好。

2023年10月28日

于成都
</content:encoded></item><item><title>Mac下编译androind下的pjsip+openh264</title><link>https://www.jason-z.com/posts/mac-make-pjsip-openh264-for-android/</link><guid isPermaLink="true">https://www.jason-z.com/posts/mac-make-pjsip-openh264-for-android/</guid><pubDate>Thu, 12 Oct 2023 06:28:23 GMT</pubDate><content:encoded>
## 概述

本文将介绍如何在 Mac 环境下编译 Android 版本的 PJSIP 和 OpenH264。编译过程中涉及设置环境变量、编译 OpenH264 和 PJSIP 的不同架构（arm 和 arm64），并最终将生成的文件集成到你的项目中。

## 编译前准备

### 设置 ANDROID NDK 环境变量

确保你已经安装了 Android NDK，并设置好环境变量：

```bash
export ANDROID_NDK_ROOT=XXXXX
```

## 编译 OpenH264

### 下载并解压 OpenH264

```bash
git clone https://github.com/cisco/openh264.git
cd openh264
```

### 编译 arm 架构

```bash
mkdir android-arm
cd build
```

#### 修改 `platform-android.mk` 文件

在文件末尾添加：

```makefile
PREFIX=android-arm
```

#### 执行编译命令

```bash
make OS=android NDKROOT=$ANDROID_NDK_ROOT TARGET=android-21 ARCH=arm NDKLEVEL=21 clean
make OS=android NDKROOT=$ANDROID_NDK_ROOT TARGET=android-21 ARCH=arm NDKLEVEL=21
make install OS=android NDKROOT=$ANDROID_NDK_ROOT TARGET=android-21 ARCH=arm NDKLEVEL=21
```

#### 复制库文件

将生成的 `libopenh264.so` 复制到你的项目中。

### 编译 arm64 架构

```bash
cd openh264
mkdir android-arm64
cd build
```

#### 修改 `platform-android.mk` 文件

在文件末尾添加：

```makefile
PREFIX=android-arm64
```

#### 执行编译命令

```bash
make OS=android NDKROOT=$ANDROID_NDK_ROOT TARGET=android-21 ARCH=arm64 NDKLEVEL=21 clean
make OS=android NDKROOT=$ANDROID_NDK_ROOT TARGET=android-21 ARCH=arm64 NDKLEVEL=21
make install OS=android NDKROOT=$ANDROID_NDK_ROOT TARGET=android-21 ARCH=arm64 NDKLEVEL=21
```

#### 复制库文件

将生成的 `libopenh264.so` 复制到你的项目中。

## 编译 PJSIP

### 下载并解压 PJSIP

```bash
git clone https://github.com/pjsip/pjproject
cd pjproject-2.13.1
```

### 编译 armeabi-v7a 架构

#### 执行配置和编译命令

```bash
TARGET_ABI=armeabi-v7a APP_PLATFORM=android-21 ./configure-android --with-openh264=/你的openh264路径/android-arm
make dep &amp;&amp; make
```

#### 编译应用程序

```bash
cd swig/src/app
make
```

#### 复制生成的文件

- **so 文件**：
  ```
  pjsip-apps/src/swig/java/android/app/src/main/jniLibs/armeabi
  ```

- **java 文件**：
  ```
  pjsip-apps/src/swig/java/android/pjsua2/src/main/java
  ```

将这些文件复制到你的项目中。

### 编译 armeabi-v8a 架构

#### 清理之前的编译

```bash
cd swig/src/app
make clean
```

#### 回到根目录并执行配置和编译命令

```bash
cd ../..
TARGET_ABI=armeabi-v8a APP_PLATFORM=android-21 ./configure-android --with-openh264=/你的openh264路径/android-arm64
make dep &amp;&amp; make
```

#### 编译应用程序

```bash
cd swig/src/app
make
```

#### 复制生成的文件

将生成的 `so` 文件复制到你的项目中。

## 总结

本文详细介绍了如何在 Mac 环境下编译 Android 版本的 PJSIP 和 OpenH264，并将生成的文件集成到你的项目中。通过分步骤的指导，确保编译过程顺利进行。希望这篇文章能帮助你在开发过程中顺利解决问题。




</content:encoded></item><item><title>2023国庆自驾都江堰-小金环线</title><link>https://www.jason-z.com/posts/2023-dujiangyan-xiaojin-travel/</link><guid isPermaLink="true">https://www.jason-z.com/posts/2023-dujiangyan-xiaojin-travel/</guid><pubDate>Fri, 06 Oct 2023 04:28:17 GMT</pubDate><content:encoded>本来之前和我姐他们约好暑假一起出去的，结果后来由于去了浙江就没走成，后面就等到了国庆总算是一起约上了，后来书院的同学们听说要出去，也说一起去，大家一拍即合，于是就来了一场说走就走的旅行。



**Day1: 都江堰-汶川-理县-马尔康-玛嘉沟 （300公里）**



![image.png](https://backend.jason-z.com/uploads/image_61544eda95.png)



为了避开出行高峰，我们刻意晚了2天出发，然后刚从都江堰上了都汶高速，就开始堵起了，前方我姐走在前面发回消息是因为紫坪隧道有车子出了故障才导致堵车，果不其然，到了映秀之后，路上就没有车辆了，这一度让我们怀疑是不是过了一个假国庆。

出发的时候，都江堰的天气，是小雨蒙蒙的，结果州内没想到这天气是格外晴朗，当时因为惧怕高原地区天气过于寒冷，我们都带的时候厚衣服，这下子竟然是感觉是热得不行。

![image.png](https://backend.jason-z.com/uploads/image_1fcfa353db.png)



![image.png](https://backend.jason-z.com/uploads/image_70edde83f6.png)

经过三个多小时的驱车，中午我们到达了马尔康市，一行人早已饥肠辘辘，于是路过一家饭店就开始了干饭🍚，虽然这家店面不大，但是味道还是不错，基本上人均干了三碗，菜盘都干得精光。

![IMG_6678.JPG](https://backend.jason-z.com/uploads/IMG_6678_22ecc8984a.JPG)

马尔康市

从马尔康出来，往小金方向走s21，一路上顺着山路往上继续爬升，中途遇到了一个不知名的山沟之处，大家索性搭起帐篷和天幕休憩一下，继续往前，会翻越一个接近4000m的垭口，那里有个山坡，视觉效果非常震撼，可惜地方太小，车太多，为了不影响后面的交通，我们没有过多停留。

![image.png](https://backend.jason-z.com/uploads/image_972c60c617.png)![image.png](https://backend.jason-z.com/uploads/image_36dfc5b5ba.png)

垭口

从垭口下来，开了有1个多小时，到达了一个叫做玛嘉沟的景区，由于中间有几个人（包括我）都有点儿高反的症状，因此大家决定就在附近住下，休息一晚，第二天再去这个景区逛逛。



**Day 2 玛嘉沟-小金-夹金山 （170公里）**



![image.png](https://backend.jason-z.com/uploads/image_e38b9f0aa5.png)

经过一夜的休整，大家的高反总算是恢复得差不多了，虽然昨夜山里下了很大的雨，但是早上起来，天气确实格外晴朗，大家决定还是按照原计划去景区看看，景区门票60元，进门需要搭乘景区的摆渡车，二十分钟山路开到山上。因为景区是刚开的，所以人并不是那么多，中间有一条步栈道，老人小孩走起来都很轻松，关键环境感觉比较原始，行走在山沟之中，两边都是高山林立，感觉很不错，拍照更是很容易出片。

![2e1d113df5af554ad7f8a31fa8ee6d41.jpg](https://backend.jason-z.com/uploads/2e1d113df5af554ad7f8a31fa8ee6d41_de80f7fb02.jpg)![0b590bdba2a0677bdcf849c45e44c973.jpg](https://backend.jason-z.com/uploads/0b590bdba2a0677bdcf849c45e44c973_db4b17e988.jpg)![457a8d2830bad67328d8ea3b0cd8c3cc.jpg](https://backend.jason-z.com/uploads/457a8d2830bad67328d8ea3b0cd8c3cc_01c583713e.jpg)![LumixSync_copy_2023-10-02
11:56:40
+0000JpegFile-66ac9fd7e563.JPG](https://backend.jason-z.com/uploads/Lumix_Sync_copy_2023_10_02_11_56_40_0000_Jpeg_File_66ac9fd7e563_59b21208ec.JPG)![19b766a1b2e73cf054ae085e92f734a1.jpg](https://backend.jason-z.com/uploads/19b766a1b2e73cf054ae085e92f734a1_54337f4e8c.jpg)

由于下午还要赶路，实际上在山腰间4100米左右还有一个月亮湖，据说需要3个小时来回，鉴于大家下午还要赶路，就放弃去爬了，中午就在景区里面吃了牛杂汤和泡面。

![image.png](https://backend.jason-z.com/uploads/image_ea819e7f5b.png)



![image.png](https://backend.jason-z.com/uploads/image_5e09f99c82.png)



![image.png](https://backend.jason-z.com/uploads/image_9c5f51aa27.png)



从景区出来之后，我们就没做停留，直接驱车往夹金山方向走，中间路过四姑娘山镇，对面的车流是真的多，而夹金山方向就没那么多，只可惜翻越夹金山的时候，大雾缭绕，能见度极低，车辆都得开着双闪才能通过，我们也没去山顶的观景台观看（因为压根啥也看不到），直接下山去了。

![image.png](https://backend.jason-z.com/uploads/image_77395e6898.png)

一路都在吃

![image.png](https://backend.jason-z.com/uploads/image_db779fbaf1.png)

夹金山的大雾

下山的弯是真的多，夹金山的最高海拔差不多4100左右，等我们下到山脚才不多2500左右，我们差不多开了1个小时才到达我们今天的住宿地，夹金山村，这个地方我姐们前两年来过，我们入住的是他们村长的房子，地方很好，就是吃饭的地方没有太多选择。虽然开了5,6个小时的车，但从高海拔下来，大家的精神状态都还不错，于是晚上的时候大家还围炉夜话了一下（其实炉子没点，哈哈）

![image.png](https://backend.jason-z.com/uploads/image_af86007eeb.png)

夹金山上的鹰

Day3 夹金山 - 宝兴 - 邛崃 - 都江堰 （232公里）

![image.png](https://backend.jason-z.com/uploads/image_d95d97bf4a.png)



早上起床问了村长，这附近有哪些好像好耍的地方，村长给我们推荐了神木垒和青江源，神木垒我们合计了一下肯定时间不够，而且也可能会不少，所以我们选择了后者，去了才知道，这个景区由于一些原因，已经暂时荒废了，但是门口的大爷还是不让我们开车进去，我们只得徒步进去。

![IMG_6737.JPG](https://backend.jason-z.com/uploads/IMG_6737_fdb4185684.JPG)

这是一条僻静的山路，以至于除了遇到当地捡菌子的人，这个山路里貌似只剩下我们，但是这样的悠长小道出片的效果也是很不错。更重要的是我们的运气还算不错，竟然在路上发现了野生的小熊猫🐼。由于临近中午，这里面又没有吃饭的地方，我们只能原路返回，到了昨天约好的店里，喝了藏家的酥油茶和铜锅，那个野菜确实巨好吃。

![image.png](https://backend.jason-z.com/uploads/image_55d38666e5.png)![image.png](https://backend.jason-z.com/uploads/image_52c3cc973f.png)

下午就是返程之路了，基本上就是连续的开车了，中途还遇到了猴群，从邛崃就和潘哥他们分道而行了，虽然一路畅通无阻，等到了都江堰也是晚上7点了，旅行正式结束。

![image.png](https://backend.jason-z.com/uploads/image_dceeeec1ef.png)



![image.png](https://backend.jason-z.com/uploads/image_84a6ea070b.png)



**总结**

  * 这条线路，虽然绕的有点远，但总结下来就是人特别少，即使像国庆这样的节日都堵车；
  * 这条线路周围也有著名景点可选，例如四姑娘山，神木垒，天台山都可以根据自己的时间安排；
  * 我们全程选择的是住当地人的住宿，吃饭可能稍微贵一些，但是味道都真的不错；
  * 这条线路其实一条红色路线，当你开车行驶在崇山峻岭之间感觉不易时，你就越能深切感受到当年红军长征时候翻越雪山的伟大之处。


</content:encoded></item><item><title>nextjs13增加rss feed订阅</title><link>https://www.jason-z.com/posts/nextjs-13-add-rss-feed/</link><guid isPermaLink="true">https://www.jason-z.com/posts/nextjs-13-add-rss-feed/</guid><pubDate>Fri, 06 Oct 2023 03:17:09 GMT</pubDate><content:encoded>

## 安装依赖

安装 `rss` 包及其类型定义：

```bash
npm install rss
npm install -D @types/rss
```

## 创建 RSS 文件

在项目中创建文件 `app/rss.xml/route.ts`，并添加以下代码：

```typescript
import api from &quot;../axios/api&quot;;
import Rss from &quot;rss&quot;;
import { getPosts } from &quot;../data/post.data&quot;;

const SITE_URL = &quot;https://www.jason-z.com&quot;;

// 截取 HTML 内容并去除标签
function truncateHTML(htmlString, maxLength) {
    const plainText = htmlString.replace(/&lt;[^&gt;]*&gt;/g, &apos;&apos;);
    return plainText.substring(0, maxLength);
}

export async function GET() {
    const posts = await getPosts();

    const feed = new Rss({
        title: &quot;张晓刚的博客&quot;,
        description: &quot;学习，记录，思考，分享&quot;,
        feed_url: `${SITE_URL}/rss.xml`,
        site_url: SITE_URL,
        language: &quot;en&quot;,
    });

    posts.forEach((post) =&gt; {
        feed.item({
            title: post.attributes.title,
            description: truncateHTML(post.attributes.content, 100),
            url: `${SITE_URL}/posts/${post.attributes.slug}`,
            guid: `${SITE_URL}/posts/${post.id}`,
            date: post.publishedAt,
        });
    });

    return new Response(feed.xml(), {
        headers: {
            &quot;Content-Type&quot;: &quot;application/xml&quot;,
        },
    });
}
```

## 访问 RSS

访问 [https://www.jason-z.com/rss.xml](https://www.jason-z.com/rss.xml) 即可查看 RSS 内容。


</content:encoded></item><item><title>next.js 13生成站点地图sitemap.xml</title><link>https://www.jason-z.com/posts/nextjs-13-generate-sitemap-xml/</link><guid isPermaLink="true">https://www.jason-z.com/posts/nextjs-13-generate-sitemap-xml/</guid><pubDate>Thu, 05 Oct 2023 12:25:15 GMT</pubDate><content:encoded>

## 适用版本

此方法适用于 Next.js 13.3 及以上版本。

## 参考代码：`app/sitemap.js`

```typescript
import api from &quot;./axios/api&quot;;

const URL = &quot;https://www.jason-z.com&quot;;

// 获取文章数据
async function getPosts() {
    try {
        const res = await api.get(&quot;/posts?sort[0]=createdAt:desc&quot;);
        if (res.status === 200) {
            return res.data.data;
        }
    } catch (e) {
        console.error(&quot;获取文章数据失败:&quot;, e);
    }

    return [];
}

export default async function sitemap() {
    const postsData = await getPosts();

    // 处理文章数据
    const posts = postsData.map(({ id, attributes }) =&gt; ({
        url: `${URL}/posts/${attributes?.slug}`,
        lastModified: attributes?.updatedAt,
    }));

    // 处理静态路由
    const routes = [&quot;&quot;, &quot;/blog&quot;].map((route) =&gt; ({
        url: `${URL}${route}`,
        lastModified: new Date().toISOString(),
    }));

    // 返回合并后的站点地图数据
    return [...routes, ...posts];
}
```

## 访问站点地图

访问 [https://www.jason-z.com/sitemap.xml](https://www.jason-z.com/sitemap.xml) 即可查看生成的站点地图。


</content:encoded></item><item><title>mybatis plus获取一条记录</title><link>https://www.jason-z.com/posts/mybatis-plus-get-one-record/</link><guid isPermaLink="true">https://www.jason-z.com/posts/mybatis-plus-get-one-record/</guid><pubDate>Fri, 29 Sep 2023 11:51:51 GMT</pubDate><content:encoded>
## 概述

在使用 MyBatis Plus 进行数据库操作时，有时需要获取单条记录。本文将介绍如何使用 MyBatis Plus 的 `getOne` 方法结合 `QueryWrapper` 来获取一条记录。

## 示例代码

### 获取单条记录的方法

```java
default xxx getOnly(QueryWrapper&lt;xxx&gt; wrapper) {
    wrapper.last(&quot;limit 1&quot;);
    return this.getOne(wrapper);
}
```

### 主要原理

主要原理是使用 `QueryWrapper` 的 `last` 方法添加 `limit 1` 条件，确保查询结果只返回一条记录。

## 详细步骤

1. **定义方法**：
   - 定义一个方法 `getOnly`，该方法接受一个 `QueryWrapper` 参数。

2. **添加 `limit 1` 条件**：
   - 使用 `wrapper.last(&quot;limit 1&quot;)` 添加 `limit 1` 条件，确保查询结果只返回一条记录。

3. **调用 `getOne` 方法**：
   - 使用 `this.getOne(wrapper)` 方法获取单条记录。

## 示例使用

假设有一个 `User` 实体类和对应的 `UserMapper` 接口：

### 实体类 `User`

```java
public class User {
    private Long id;
    private String name;
    private Integer age;
    // 省略 getter 和 setter 方法
}
```

### Mapper 接口 `UserMapper`

```java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

public interface UserMapper extends BaseMapper&lt;User&gt; {
    default User getOnly(QueryWrapper&lt;User&gt; wrapper) {
        wrapper.last(&quot;limit 1&quot;);
        return this.getOne(wrapper);
    }
}
```

### 使用示例

```java
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

public class UserService {
    private UserMapper userMapper;

    public User getUserByName(String name) {
        QueryWrapper&lt;User&gt; wrapper = new QueryWrapper&lt;&gt;();
        wrapper.eq(&quot;name&quot;, name);
        return userMapper.getOnly(wrapper);
    }
}
```


</content:encoded></item><item><title>Spring Boot 实现本地文件上传</title><link>https://www.jason-z.com/posts/spring-boot-upload-file/</link><guid isPermaLink="true">https://www.jason-z.com/posts/spring-boot-upload-file/</guid><pubDate>Sat, 23 Sep 2023 03:41:53 GMT</pubDate><content:encoded>

本文介绍如何在 Spring Boot 应用中实现本地文件上传功能。包括配置文件设置、配置类、控制器、响应类和服务类的实现。

## 1. 设置上传文件配置

在 `application.yml` 中配置文件上传的相关参数：

```yaml
spring:
  servlet:
    multipart:
      max-file-size: 5MB
      max-request-size: 10MB

# 上传目录
file:
  upload-dir: /Users/jasonz/Downloads/qxt
```

## 2. 配置类

### FileStorageProperties

定义一个配置类来读取上传目录的配置：

```java
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = &quot;file&quot;)
public class FileStorageProperties {
    private String uploadDir;

    public String getUploadDir() {
        return uploadDir;
    }

    public void setUploadDir(String uploadDir) {
        this.uploadDir = uploadDir;
    }
}
```

## 3. 开启配置读取

在应用程序主类中启用配置读取：

```java
import com.example.filedemo.property.FileStorageProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties({
    FileStorageProperties.class
})
public class FileDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(FileDemoApplication.class, args);
    }
}
```

## 4. 控制器

### FileController

创建一个控制器来处理文件上传和下载请求：

```java
import com.example.filedemo.payload.UploadFileResponse;
import com.example.filedemo.service.FileStorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@RestController
public class FileController {
    private static final Logger logger = LoggerFactory.getLogger(FileController.class);

    @Autowired
    private FileStorageService fileStorageService;

    @PostMapping(&quot;/uploadFile&quot;)
    public UploadFileResponse uploadFile(@RequestParam(&quot;file&quot;) MultipartFile file) {
        String fileName = fileStorageService.storeFile(file);
        String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path(&quot;/downloadFile/&quot;)
                .path(fileName)
                .toUriString();
        return new UploadFileResponse(fileName, fileDownloadUri, file.getContentType(), file.getSize());
    }

    @PostMapping(&quot;/uploadMultipleFiles&quot;)
    public List&lt;UploadFileResponse&gt; uploadMultipleFiles(@RequestParam(&quot;files&quot;) MultipartFile[] files) {
        return Arrays.asList(files)
                .stream()
                .map(file -&gt; uploadFile(file))
                .collect(Collectors.toList());
    }

    @GetMapping(&quot;/downloadFile/{fileName:.+}&quot;)
    public ResponseEntity&lt;Resource&gt; downloadFile(@PathVariable String fileName, HttpServletRequest request) {
        // Load file as Resource
        Resource resource = fileStorageService.loadFileAsResource(fileName);

        // Try to determine file&apos;s content type
        String contentType = null;
        try {
            contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        } catch (IOException ex) {
            logger.info(&quot;Could not determine file type.&quot;);
        }

        // Fallback to the default content type if type could not be determined
        if (contentType == null) {
            contentType = &quot;application/octet-stream&quot;;
        }

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, &quot;attachment; filename=\&quot;&quot; + resource.getFilename() + &quot;\&quot;&quot;)
                .body(resource);
    }
}
```

## 5. 响应类

### UploadFileResponse

定义一个响应类用于返回上传文件的信息：

```java
public class UploadFileResponse {
    private String fileName;
    private String fileDownloadUri;
    private String fileType;
    private long size;

    public UploadFileResponse(String fileName, String fileDownloadUri, String fileType, long size) {
        this.fileName = fileName;
        this.fileDownloadUri = fileDownloadUri;
        this.fileType = fileType;
        this.size = size;
    }

    // Getters and Setters (Omitted for brevity)
}
```

## 6. 服务类

### FileStorageService

实现文件存储和加载的服务类：

```java
import com.example.filedemo.exception.FileStorageException;
import com.example.filedemo.exception.MyFileNotFoundException;
import com.example.filedemo.property.FileStorageProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

@Service
public class FileStorageService {
    private final Path fileStorageLocation;

    @Autowired
    public FileStorageService(FileStorageProperties fileStorageProperties) {
        this.fileStorageLocation = Paths.get(fileStorageProperties.getUploadDir())
                .toAbsolutePath().normalize();

        try {
            Files.createDirectories(this.fileStorageLocation);
        } catch (Exception ex) {
            throw new FileStorageException(&quot;Could not create the directory where the uploaded files will be stored.&quot;, ex);
        }
    }

    public String storeFile(MultipartFile file) {
        // Normalize file name
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());

        try {
            // Check if the file&apos;s name contains invalid characters
            if (fileName.contains(&quot;..&quot;)) {
                throw new FileStorageException(&quot;Sorry! Filename contains invalid path sequence &quot; + fileName);
            }

            // Copy file to the target location (Replacing existing file with the same name)
            Path targetLocation = this.fileStorageLocation.resolve(fileName);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
            return fileName;
        } catch (IOException ex) {
            throw new FileStorageException(&quot;Could not store file &quot; + fileName + &quot;. Please try again!&quot;, ex);
        }
    }

    public Resource loadFileAsResource(String fileName) {
        try {
            Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());

            if (resource.exists()) {
                return resource;
            } else {
                throw new MyFileNotFoundException(&quot;File not found &quot; + fileName);
            }
        } catch (MalformedURLException ex) {
            throw new MyFileNotFoundException(&quot;File not found &quot; + fileName, ex);
        }
    }
}
```

## 7. 异常类

### FileStorageException

自定义异常类用于处理文件存储错误：

```java
public class FileStorageException extends RuntimeException {
    public FileStorageException(String message) {
        super(message);
    }

    public FileStorageException(String message, Throwable cause) {
        super(message, cause);
    }
}
```

### MyFileNotFoundException

自定义异常类用于处理文件未找到错误：

```java
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class MyFileNotFoundException extends RuntimeException {
    public MyFileNotFoundException(String message) {
        super(message);
    }

    public MyFileNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}
```



</content:encoded></item><item><title>Spring boot树状数据工具类</title><link>https://www.jason-z.com/posts/spring-boot-tree-data-tool-util/</link><guid isPermaLink="true">https://www.jason-z.com/posts/spring-boot-tree-data-tool-util/</guid><pubDate>Sat, 23 Sep 2023 03:16:59 GMT</pubDate><content:encoded>
## 简介

在实际项目开发中，例如构造菜单或组织架构的数据时，经常会需要返回树状的层级数据。解决这个问题可以通过构造 SQL 语句或者递归来进行构造。本文推荐使用递归方式来构建树状数据。

## 数据模型

假设数据表使用 `parentId` 字段进行父子级关联。定义一个 VO 类来表示树节点：

```java
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(description = &quot;TreeVO&quot;)
public class TreeVO {

    @ApiModelProperty(value = &quot;主键id&quot;)
    private long id;

    @ApiModelProperty(value = &quot;父id&quot;)
    private long parentId;

    @ApiModelProperty(value = &quot;子节点&quot;)
    private List&lt;TreeVO&gt; children = new ArrayList&lt;&gt;();
}
```

## 工具类

### 构建树节点

通过递归方法构建树状结构：

```java
import java.util.*;

public class TreeUtil {

    /**
     * 根据 pid 构建树节点
     */
    public static &lt;T extends TreeVO&gt; List&lt;T&gt; build(List&lt;T&gt; treeNodes, Integer pid) {
        List&lt;T&gt; treeList = new ArrayList&lt;&gt;();
        for (T treeNode : treeNodes) {
            if (pid.equals(treeNode.getParentId())) {
                treeList.add(findChildren(treeNodes, treeNode));
            }
        }
        return treeList;
    }

    /**
     * 查找子节点
     */
    private static &lt;T extends TreeVO&gt; T findChildren(List&lt;T&gt; treeNodes, T rootNode) {
        for (T treeNode : treeNodes) {
            if (rootNode.getId().equals(treeNode.getParentId())) {
                rootNode.getChildren().add(findChildren(treeNodes, treeNode));
            }
        }
        return rootNode;
    }

    /**
     * 构建树节点（无参版本）
     */
    public static &lt;T extends TreeVO&gt; List&lt;T&gt; build(List&lt;T&gt; treeNodes) {
        List&lt;T&gt; result = new ArrayList&lt;&gt;();
        // list 转 map
        Map&lt;Long, T&gt; nodeMap = new LinkedHashMap&lt;&gt;(treeNodes.size());
        for (T treeNode : treeNodes) {
            nodeMap.put(treeNode.getId(), treeNode);
        }

        for (T node : nodeMap.values()) {
            T parent = nodeMap.get(node.getParentId());
            if (parent != null &amp;&amp; !node.getId().equals(parent.getId())) {
                parent.getChildren().add(node);
                continue;
            }
            result.add(node);
        }
        return result;
    }
}
```

## 使用示例

假设你有一个包含多个 `TreeVO` 对象的列表 `treeNodes`，你可以通过以下方式构建树状结构：

```java
List&lt;TreeVO&gt; treeNodes = // 获取你的数据列表
List&lt;TreeVO&gt; tree = TreeUtil.build(treeNodes, 0); // 假设根节点的 parentId 为 0
```



</content:encoded></item><item><title>MyBatis Plus List数据转Page</title><link>https://www.jason-z.com/posts/mybatis-plus-list-to-page/</link><guid isPermaLink="true">https://www.jason-z.com/posts/mybatis-plus-list-to-page/</guid><pubDate>Sat, 23 Sep 2023 03:05:42 GMT</pubDate><content:encoded>

## 概述

在实际开发中，经常会遇到将 `List&lt;T&gt;` 数据转换为 MyBatis Plus 的 `Page&lt;T&gt;` 对象的需求。本文将介绍一个实用的工具类来实现这一转换。

## 工具类示例

以下是一个示例工具类 `PageUtil`，用于将 `List&lt;T&gt;` 数据转换为 `Page&lt;T&gt;` 对象。

```java
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import java.util.ArrayList;
import java.util.List;

public class PageUtil {

    public static &lt;T&gt; Page&lt;T&gt; getPages(Integer pageNo, Integer pageSize, List&lt;T&gt; list) {
        Page&lt;T&gt; page = new Page&lt;&gt;();
        int size = list.size();
        
        // 如果 pageSize 大于列表大小，调整 pageSize
        if (pageSize &gt; size) {
            pageSize = size;
        }
        
        // 计算最大页数，防止 pageNo 越界
        int maxPage = size % pageSize == 0 ? size / pageSize : size / pageSize + 1;
        if (pageNo &gt; maxPage) {
            pageNo = maxPage;
        }
        
        // 计算当前页第一条数据的下标
        int currentIndex = pageNo &gt; 1 ? (pageNo - 1) * pageSize : 0;
        List&lt;T&gt; pageList = new ArrayList&lt;&gt;();
        
        // 将当前页的数据添加到 pageList
        for (int i = 0; i &lt; pageSize &amp;&amp; currentIndex + i &lt; size; i++) {
            pageList.add(list.get(currentIndex + i));
        }
        
        page.setCurrent(pageNo).setSize(pageSize).setTotal(size).setRecords(pageList);
        return page;
    }
}
```

## 使用示例

### 实体类 `User`

```java
public class User {
    private Long id;
    private String name;
    private Integer age;
    // 省略 getter 和 setter 方法
}
```

### 使用工具类

```java
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import java.util.ArrayList;
import java.util.List;

public class UserService {
    public Page&lt;User&gt; getUsersPage(Integer pageNo, Integer pageSize) {
        List&lt;User&gt; userList = new ArrayList&lt;&gt;();
        // 假设 userList 已经填充了数据
        userList.add(new User(1L, &quot;Alice&quot;, 30));
        userList.add(new User(2L, &quot;Bob&quot;, 25));
        userList.add(new User(3L, &quot;Charlie&quot;, 35));
        
        return PageUtil.getPages(pageNo, pageSize, userList);
    }
}
```

## 详细步骤

1. **定义工具类**：
   - 创建一个工具类 `PageUtil`，其中包含 `getPages` 方法。

2. **实现 `getPages` 方法**：
   - 计算列表的总大小 `size`。
   - 如果 `pageSize` 大于列表大小，调整 `pageSize`。
   - 计算最大页数 `maxPage`，防止 `pageNo` 越界。
   - 计算当前页第一条数据的下标 `currentIndex`。
   - 将当前页的数据添加到 `pageList`。
   - 设置 `Page` 对象的当前页、每页大小、总记录数和记录列表。

3. **使用工具类**：
   - 在服务层或其他业务逻辑中调用 `PageUtil.getPages` 方法，传入页码 `pageNo`、每页大小 `pageSize` 和数据列表 `list`。

</content:encoded></item><item><title>Spring boot格式化LocaleDateTime</title><link>https://www.jason-z.com/posts/spring-boot-format-datetime/</link><guid isPermaLink="true">https://www.jason-z.com/posts/spring-boot-format-datetime/</guid><pubDate>Fri, 22 Sep 2023 09:20:29 GMT</pubDate><content:encoded>
## 简介

在 Spring Boot 中，默认情况下实体类中的 `LocalDateTime` 字段在输出为 JSON 时会使用 ISO 8601 格式。为了自定义日期时间格式，可以通过注解或全局配置来实现。

## 单个字段格式化

可以使用注解对单个字段进行转换：

```java
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;)
@JsonProperty(&quot;date&quot;)
LocalDateTime getDate();
```

## 全局配置

### application.yml 配置

通过 `application.yml` 文件配置全局日期格式：

```yaml
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
```

### 配置类

通过配置类自定义 `LocalDateTime` 的序列化和反序列化行为：

```java
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Configuration
public class LocalDateTimeConfig {

    @Value(&quot;${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}&quot;)
    private String pattern;

    // LocalDateTime 序列化器
    @Bean
    public LocalDateTimeSerializer localDateTimeSerializer() {
        return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
    }

    // LocalDateTime 反序列化器
    @Bean
    public LocalDateTimeDeserializer localDateTimeDeserializer() {
        return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(pattern));
    }

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        return builder -&gt; {
            builder.serializerByType(LocalDateTime.class, localDateTimeSerializer());
            builder.deserializerByType(LocalDateTime.class, localDateTimeDeserializer());
            builder.simpleDateFormat(pattern);
        };
    }
}
```


</content:encoded></item><item><title>git提交ERROR This version of pnpm requires at least Node.js v16.4错误解决方案</title><link>https://www.jason-z.com/posts/git-commit-error-this-version-of-pnpm-requires-at-least-node-js-v16-4-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/git-commit-error-this-version-of-pnpm-requires-at-least-node-js-v16-4-solution/</guid><pubDate>Mon, 18 Sep 2023 09:04:51 GMT</pubDate><content:encoded>
## 概述

在使用 Git 提交时，可能会遇到以下错误提示：

```
ERROR: This version of pnpm requires at least Node.js v16.14
The current version of Node.js is v16.10.0
Visit https://r.pnpm.io/comp to see the list of past pnpm versions with respective Node.js version support.
husky - pre-commit hook exited with code 1 (error)
```

该错误表示当前安装的 Node.js 版本低于 pnpm 所需的最低版本要求。本文将介绍如何解决这一问题。

## 问题原因

此错误主要是因为 `husky` 插件在提交前会进行一些校验，确保项目依赖和环境配置符合要求。如果 Node.js 版本不符合 pnpm 的最低要求，就会触发上述错误。

## 解决办法

### 方法一：临时关闭校验

如果你需要快速提交而不想立即解决版本问题，可以临时关闭 `husky` 校验。

#### 示例命令

```bash
git commit -m &apos;提交信息&apos; --no-verify
```

**注意**：这种方法适用于紧急情况，但不推荐长期使用，建议尽快升级 Node.js 版本以符合项目要求。

### 方法二：关闭 Git Hook

如果你希望完全禁用 Git Hook（例如 husky），可以执行以下命令：

#### 示例命令

```bash
git config --unset core.hooksPath
```

**注意**：这种方法较为激进，会禁用所有 Git Hook，可能影响项目的自动化流程，建议谨慎使用。

</content:encoded></item><item><title>Vite代理下获取真实请求的URL</title><link>https://www.jason-z.com/posts/vite-proxy-get-real-url/</link><guid isPermaLink="true">https://www.jason-z.com/posts/vite-proxy-get-real-url/</guid><pubDate>Mon, 18 Sep 2023 08:32:15 GMT</pubDate><content:encoded>

## 概述

在使用 **Vite** 进行本地代理时，真实请求的地址会被本地代理转发。平时开发中使用 **Chrome DevTools** 时，无法直接看到这个真实地址，这给问题定位带来了不便。

我们可以通过 Vite 提供的 `bypass` 函数来实现对真实请求 URL 的捕获和记录。

## 实现方法

### 配置 Vite 代理

在 `vite.config.ts` 中配置代理，并通过 `bypass` 函数捕获并打印真实请求的 URL：

```typescript
import { defineConfig } from &apos;vite&apos;;

export default defineConfig({
  server: {
    proxy: {
      &apos;/api&apos;: {
        target: &apos;http://localhost:8050&apos;,
        changeOrigin: true,
        ws: true,
        rewrite: (path) =&gt; path.replace(/^\/api/, &apos;&apos;),
        bypass(req, res, options) {
          const proxyUrl = new URL(options.rewrite(req.url) || &apos;&apos;, options.target as string).href;
          console.log(&apos;Proxy URL:&apos;, proxyUrl);
        },
        // only https
        // secure: false
      }
    }
  }
});
```

### 将 URL 写入响应头

为了在实际调试时更加直观，可以将捕获到的真实 URL 写入响应头：

```typescript
bypass(req, res, options) {
  const proxyUrl = new URL(options.rewrite(req.url) || &apos;&apos;, options.target as string).href;
  console.log(&apos;Proxy URL:&apos;, proxyUrl);
  res.setHeader(&apos;X-Proxy-URL&apos;, proxyUrl);
}
```

这样，在使用 **Chrome DevTools** 查看网络请求时，可以直接在响应头中看到 `X-Proxy-URL` 字段，方便调试。
</content:encoded></item><item><title>Could not resolve com.android.tools.build:gradle:8.0.1 错误的解决办法</title><link>https://www.jason-z.com/posts/could-not-resolve-com-android-tools-build-gradle-8-0-1-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/could-not-resolve-com-android-tools-build-gradle-8-0-1-solution/</guid><pubDate>Fri, 15 Sep 2023 14:39:24 GMT</pubDate><content:encoded>
## 概述

在使用最新版本的 Android Studio（例如 2022.2.1 patch 1）进行项目编辑时，可能会遇到以下错误提示：

```
* What went wrong:
A problem occurred configuring root project &apos;xxxx&apos;.
&gt; Could not resolve all files for configuration &apos;:classpath&apos;.
   &gt; Could not resolve com.android.tools.build:gradle:8.0.1.
```

该错误通常表示 Gradle 无法解析 `com.android.tools.build:gradle:8.0.1` 依赖项。本文将介绍如何解决这一问题。

## 解决办法

### 更换 JDK 版本为 17 或 11

#### macOS 下

1. **打开 Android Studio**：
   - 启动 Android Studio 并进入主界面。

2. **进入设置页面**：
   - 点击菜单栏中的 `Android Studio` -&gt; `Preferences`。

3. **配置 Gradle 设置**：
   - 在左侧导航栏中选择 `Build, Execution, Deployment` -&gt; `Build Tools` -&gt; `Gradle`。
   - 将 `Gradle JDK` 设置为版本 17 或 11。

#### Windows 下

1. **打开 Android Studio**：
   - 启动 Android Studio 并进入主界面。

2. **进入设置页面**：
   - 点击菜单栏中的 `File` -&gt; `Settings`。

3. **配置 Gradle 设置**：
   - 在左侧导航栏中选择 `Build, Execution, Deployment` -&gt; `Build Tools` -&gt; `Gradle`。
   - 将 `Gradle JDK` 设置为版本 17 或 11。

### 示例截图

![配置 Gradle JDK](https://i.stack.imgur.com/w2uV9.png)

### 注意事项

- **JDK 版本兼容性**：确保你使用的 JDK 版本与当前项目的 Gradle 插件版本兼容。建议使用官方推荐的 JDK 版本。
- **Gradle 缓存清理**：如果问题仍然存在，尝试清理 Gradle 缓存并重新同步项目。
- **网络连接**：确保你的网络连接正常，以避免因网络问题导致的依赖项下载失败。
</content:encoded></item><item><title>Android自定义view提示Caused by java.lang.NoSuchMethodException错误的解决办法</title><link>https://www.jason-z.com/posts/android-view-caused-by-java-lang-no-such-method-exception-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/android-view-caused-by-java-lang-no-such-method-exception-solution/</guid><pubDate>Fri, 15 Sep 2023 14:17:14 GMT</pubDate><content:encoded>
## 概述

在开发 Android 项目时，自定义 View 是一个常见的需求。然而，有时会遇到以下错误提示：

```
Caused by: java.lang.NoSuchMethodException: &lt;init&gt; [class android.content.Context, interface android.util.AttributeSet]
```

该错误通常表示自定义 View 缺少必要的构造函数，导致初始化失败。本文将介绍如何解决这一问题。

## 问题原因

出现此错误的原因是自定义 View 没有重写带有 `(Context context, AttributeSet attrs)` 参数的构造函数。当系统尝试通过 XML 布局文件实例化自定义 View 时，找不到匹配的构造函数，从而抛出 `NoSuchMethodException` 异常。

## 解决办法

### 添加必要的构造函数

为了解决这个问题，需要确保自定义 View 类中包含所有必要的构造函数。特别是带有 `(Context context, AttributeSet attrs)` 参数的构造函数。

#### 示例代码

假设你有一个名为 `ControlKeyboardLinearLayout` 的自定义 View 类，可以按照以下方式添加构造函数：

```java
public class ControlKeyboardLinearLayout extends LinearLayout {

    // 默认构造函数（无参数）
    public ControlKeyboardLinearLayout(Context context) {
        super(context);
    }

    // 带有 AttributeSet 参数的构造函数
    public ControlKeyboardLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // 带有 AttributeSet 和 defStyleAttr 参数的构造函数（可选）
    public ControlKeyboardLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    // 带有 AttributeSet、defStyleAttr 和 defStyleRes 参数的构造函数（可选）
    public ControlKeyboardLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}
```

### 注意事项

- **构造函数完整性**：确保自定义 View 类中包含所有可能用到的构造函数，以避免类似的问题。
- **调用父类构造函数**：在每个构造函数中调用相应的父类构造函数（如 `super(context, attrs)`），以确保正确初始化。
- **XML 布局文件**：确保在 XML 布局文件中正确引用自定义 View，并传递必要的属性。


</content:encoded></item><item><title>Android在debug模式下配置签名</title><link>https://www.jason-z.com/posts/android-debug-sign-configure/</link><guid isPermaLink="true">https://www.jason-z.com/posts/android-debug-sign-configure/</guid><pubDate>Fri, 15 Sep 2023 13:13:54 GMT</pubDate><content:encoded>
## 概述

在开发过程中，有时需要为应用程序配置调试模式下的签名文件，尤其是在测试某些依赖于签名的 SDK 时。本文将介绍如何在 `debug` 模式下配置签名文件。

## 修改 build.gradle 文件

要在 `debug` 模式下配置签名文件，需要修改 **App module** 的 `build.gradle` 文件。具体步骤如下：

### 1. 配置签名信息

在 `android` 块中添加 `signingConfigs` 配置，指定签名文件的相关信息：

```groovy
android {
    signingConfigs {
        debug {
            // 签名文件路径
            storeFile file(&quot;debugtest.jks&quot;)
            // 签名密码
            storePassword &quot;android&quot;
            // 别名
            keyAlias &quot;mykey&quot;
            // 别名密码
            keyPassword &quot;android&quot;
        }
    }
}
```

### 2. 应用签名配置到 debug 构建类型

接下来，在 `buildTypes` 中应用上述签名配置到 `debug` 构建类型，并启用代码混淆（可选）：

```groovy
buildTypes {
    debug {
        signingConfig signingConfigs.debug
        minifyEnabled true
        proguardFiles getDefaultProguardFile(&apos;proguard-android-optimize.txt&apos;), &apos;proguard-rules.pro&apos;
    }
}
```

### 注意事项

- **签名文件路径**：确保 `debugtest.jks` 文件路径正确，可以是相对路径或绝对路径。
- **代码混淆**：`minifyEnabled true` 启用了 ProGuard 进行代码混淆，这在调试时可能会增加复杂性。如果不需要混淆，可以将其设置为 `false`。

## 总结

通过以上步骤，你可以在 `debug` 模式下为应用程序配置签名文件。这对于测试某些依赖于签名的 SDK 或其他需要签名验证的功能非常有用。


</content:encoded></item><item><title>Strapi实现根据slug查找记录</title><link>https://www.jason-z.com/posts/strapi-find-by-slug/</link><guid isPermaLink="true">https://www.jason-z.com/posts/strapi-find-by-slug/</guid><pubDate>Fri, 15 Sep 2023 13:01:04 GMT</pubDate><content:encoded>
## 简介

Strapi 默认情况下是通过 `id` 来查找单条记录的。在博客系统中，我们通常需要通过 `slug` 字段进行查询。本文以 Posts 为例，介绍如何实现根据 `slug` 查找记录。

假设你已经在数据模型中添加了 `slug` 字段，本文将不再赘述其创建过程。

## 步骤

### 第一步：定义路由

在源码目录 `src/api/post/routes` 下新建一个 `_custom.js` 文件，定义新的路由：

```javascript
module.exports = {
  routes: [
    {
      method: &apos;GET&apos;,
      path: &apos;/posts/:slug&apos;,
      handler: &apos;post.findOne&apos;,
      config: {
        auth: false, // 根据需求配置认证
      },
    }
  ]
};
```

### 第二步：实现控制器方法

修改源码目录 `src/api/post/controllers` 下的 `post.js` 文件，添加 `findOne` 方法：

```javascript
&apos;use strict&apos;;

/**
 * post controller
 */

const { createCoreController } = require(&apos;@strapi/strapi&apos;).factories;

module.exports = createCoreController(&apos;api::post.post&apos;, ({ strapi }) =&gt; ({
  async findOne(ctx) {
    const { slug } = ctx.params;
    const { query } = ctx;

    try {
      const entity = await strapi.db.query(&apos;api::post.post&apos;).findOne({
        where: { slug },
        populate: query.populate ? query.populate.split(&apos;,&apos;) : [], // 根据需要处理关联字段
      });

      if (!entity) {
        return ctx.notFound(&apos;Post not found&apos;);
      }

      const sanitizedEntity = await this.sanitizeOutput(entity);

      return this.transformResponse(sanitizedEntity);
    } catch (error) {
      return ctx.badRequest(null, [{ messages: [{ id: error.message }] }]);
    }
  },
}));
```

### 测试

重启 Strapi 控制台后，请求 `/api/posts/文章id` 将会返回 404，而请求 `/api/posts/文章slug` 将会返回对应的记录。



</content:encoded></item><item><title>Nextjs13 引入Prism js</title><link>https://www.jason-z.com/posts/nextjs-13-import-prism-js/</link><guid isPermaLink="true">https://www.jason-z.com/posts/nextjs-13-import-prism-js/</guid><pubDate>Fri, 15 Sep 2023 10:17:33 GMT</pubDate><content:encoded>

## 简介

在博客项目中准备使用 Prism.js 对代码进行高亮。目前只能通过客户端组件的方式实现。

## 安装 Prism.js

安装 Prism.js 及其依赖：

```bash
npm install prismjs
```

## 使用 `useEffect` 高亮代码

在客户端组件中使用 `useEffect` 来高亮代码：

```typescript
&quot;use client&quot;;

import { useEffect } from &quot;react&quot;;
import Prism from &quot;prismjs&quot;;

const Post = ({ post }) =&gt; {
  useEffect(() =&gt; {
    const highlight = async () =&gt; {
      await Prism.highlightAll();
    };
    highlight();
  }, [post]);

  return (
    // 组件内容
  );
};
```

## 引入主题

引入预定义的主题 CSS 文件：

```typescript
import &quot;prismjs/themes/prism-tomorrow.css&quot;;
```

如果需要其他主题，可以自行下载并引入：

[Prism 主题仓库](https://github.com/PrismJS/prism-themes/tree/master/themes)

## 加载语言支持

根据需要加载特定的语言支持：

```typescript
import &apos;prismjs/components/prism-javascript&apos;;
import &apos;prismjs/components/prism-css&apos;;
import &apos;prismjs/components/prism-jsx&apos;;
import &apos;prismjs/components/prism-typescript&apos;;
```

## 自定义主题

### 全局语言样式

```css
pre[class*=&quot;language-&quot;],
code[class*=&quot;language-&quot;] {
  /* 自定义样式 */
}
```

### 特定语言样式

```css
pre[class*=&quot;language-javascript&quot;],
code[class*=&quot;language-javascript&quot;] {
  color: #4ec9b0;
}
```

### Token 样式

```css
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
  color: #6a9955;
}

.token.punctuation {
  color: #d4d4d4;
}

.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
  color: #b5cea8;
}
```

更多设置请参考 [Prism.js 常见问题](https://prismjs.com/faq.html#how-do-i-know-which-tokens-i-can-style-for)。
```

</content:encoded></item><item><title>Minified React error错误的解决办法</title><link>https://www.jason-z.com/posts/minified-react-error-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/minified-react-error-solution/</guid><pubDate>Fri, 15 Sep 2023 06:35:19 GMT</pubDate><content:encoded>

## 概述

在使用 Next.js 项目时，可能会遇到以下错误提示：

```
596-e2c30dd291e774da.js:1 Error: Minified React error #321; visit https://reactjs.org/docs/error-decoder.html?invariant=321 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
```

该错误通常是由于 React 或其相关库的配置问题引起的。本文将介绍如何解决这一问题。

## 错误原因

根据 React 官方文档的提示，Minified React 错误 #321 可能由以下三种情况造成：

1. **确定 `react` 和 `react-dom` 是相同版本**：
   - 确保项目中 `react` 和 `react-dom` 的版本一致。

2. **确定自己的代码没有违背 Hooks 的使用准则**：
   - 确保在代码中正确使用 React Hooks，遵循 Hooks 的规则。

3. **确定项目中是否引入了多个 `react` 实例**：
   - 确保项目中没有重复引入多个 `react` 实例，这可能导致冲突。

## 解决办法

### 方法一：确保 `react` 和 `react-dom` 版本一致

1. **检查 `package.json` 文件**：
   - 确保 `react` 和 `react-dom` 的版本相同。

   ```json
   &quot;dependencies&quot;: {
     &quot;react&quot;: &quot;^18.2.0&quot;,
     &quot;react-dom&quot;: &quot;^18.2.0&quot;
   }
   ```

2. **安装依赖**：
   - 运行以下命令安装或更新依赖。

   ```bash
   npm install
   ```

### 方法二：确保 Hooks 使用正确

根据 React 官方文档，Hooks 的使用准则包括：

- **只在顶层调用 Hooks**：不要在循环、条件或嵌套函数中调用 Hooks。
- **只在 React 函数组件或自定义 Hooks 中调用 Hooks**：不要在普通的 JavaScript 函数中调用 Hooks。

#### 示例代码

**正确使用 Hooks**：

```jsx
import React, { useState, useEffect } from &apos;react&apos;;

function ExampleComponent() {
  const [count, setCount] = useState(0);

  useEffect(() =&gt; {
    document.title = `You clicked ${count} times`;
  }, [count]);

  return (
    &lt;div&gt;
      &lt;p&gt;You clicked {count} times&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
        Click me
      &lt;/button&gt;
    &lt;/div&gt;
  );
}
```

**错误使用 Hooks**：

```jsx
import React, { useState, useEffect } from &apos;react&apos;;

function ExampleComponent() {
  if (count &gt; 0) {
    const [count, setCount] = useState(0); // 错误：在条件语句中调用 Hooks
  }

  useEffect(() =&gt; {
    document.title = `You clicked ${count} times`;
  }, [count]);

  return (
    &lt;div&gt;
      &lt;p&gt;You clicked {count} times&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
        Click me
      &lt;/button&gt;
    &lt;/div&gt;
  );
}
```

### 方法三：确保没有引入多个 `react` 实例

1. **检查 `node_modules` 中的 `react` 实例**：
   - 确保项目中没有重复的 `react` 实例。

2. **使用 `npm ls react` 检查依赖树**：
   - 运行以下命令检查项目中 `react` 的依赖树。

   ```bash
   npm ls react
   ```

3. **解决重复依赖**：
   - 如果发现重复的 `react` 实例，可以通过以下方法解决：
     - **删除 `node_modules` 并重新安装依赖**：

       ```bash
       rm -rf node_modules
       npm install
       ```

     - **使用 `resolutions` 字段**（适用于 Yarn）：

       ```json
       &quot;resolutions&quot;: {
         &quot;react&quot;: &quot;^18.2.0&quot;,
         &quot;react-dom&quot;: &quot;^18.2.0&quot;
       }
       ```

     - **使用 `npm dedupe`**：

       ```bash
       npm dedupe
       ```

## 实际排查

在你的项目中，实际排查发现问题是由于 Hooks 使用不当造成的。具体来说，某些 Hooks 在条件语句中被调用，违反了 Hooks 的使用准则。移除这些不当的 Hooks 调用后，问题得到解决。

### 示例代码

**错误代码**：

```jsx
import React, { useState } from &apos;react&apos;;

function ExampleComponent() {
  if (true) {
    const [count, setCount] = useState(0); // 错误：在条件语句中调用 Hooks
  }

  return (
    &lt;div&gt;
      &lt;p&gt;You clicked {count} times&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
        Click me
      &lt;/button&gt;
    &lt;/div&gt;
  );
}
```

**修正后的代码**：

```jsx
import React, { useState } from &apos;react&apos;;

function ExampleComponent() {
  const [count, setCount] = useState(0); // 正确：在顶层调用 Hooks

  return (
    &lt;div&gt;
      &lt;p&gt;You clicked {count} times&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
        Click me
      &lt;/button&gt;
    &lt;/div&gt;
  );
}
```



</content:encoded></item><item><title>g++ internal compiler error Killed错误的解决方案</title><link>https://www.jason-z.com/posts/g-internal-compiler-error-killed-solution/</link><guid isPermaLink="true">https://www.jason-z.com/posts/g-internal-compiler-error-killed-solution/</guid><pubDate>Wed, 13 Sep 2023 12:45:46 GMT</pubDate><content:encoded>
## 概述

在使用 `gcc` 或 `g++` 进行编译时，有时会遇到以下错误提示：

```
g++: internal compiler error: Killed (program cc1plus)
Please submit a full bug report,
with preprocessed source if appropriate.
```

该错误通常表示编译过程中出现了内部编译器错误，并且程序被系统终止（Killed）。本文将介绍如何解决这一问题。

## 问题原因

导致此错误的主要原因是编译时系统内存不足。当编译任务占用过多内存时，操作系统可能会终止编译进程以防止系统崩溃。

## 解决办法

### 方法一：增加系统内存

如果条件允许，可以考虑升级硬件配置，增加物理内存（RAM），以确保编译过程有足够的内存可用。

### 方法二：降低编译并行度

降低编译的并行线程数可以减少内存占用，从而避免因内存不足导致的错误。可以通过调整 `make` 命令中的 `-j` 参数来控制并行编译的线程数。

#### 示例命令

- **4 个并行线程**：

  ```bash
  make -j4
  ```

- **8 个并行线程**：

  ```bash
  make -j8
  ```

**注意**：减少并行线程数虽然可以避免内存不足的问题，但也会延长编译时间。根据系统的实际内存情况选择合适的并行线程数。

### 方法三：优化编译选项

通过优化编译选项，可以进一步减少内存占用。例如，禁用某些不必要的优化选项或调试信息。

#### 示例命令

```bash
CXXFLAGS=&quot;-O2 -g0&quot; make
```

- `-O2`：启用二级优化。
- `-g0`：不生成调试信息。

### 方法四：检查系统资源

确保系统有足够的可用资源，包括但不限于：

- **交换空间（Swap）**：如果物理内存不足，可以增加交换空间作为临时解决方案。
  
  ```bash
  sudo fallocate -l 4G /swapfile
  sudo chmod 600 /swapfile
  sudo mkswap /swapfile
  sudo swapon /swapfile
  ```

- **关闭不必要的后台进程**：释放更多系统资源给编译任务。

## 注意事项

- **备份重要文件**：在修改系统配置或编译参数之前，建议先备份重要文件，以防出现问题时可以恢复。
- **权限管理**：确保你有足够的权限来执行相关命令，必要时使用 `sudo` 提升权限。
- **依赖项检查**：确保所有依赖项均已正确安装，特别是与编译工具链相关的包。

## 总结

本文介绍了如何解决 `g++: internal compiler error: Killed` 错误的方法。通过增加系统内存、降低编译并行度、优化编译选项以及检查系统资源，可以有效避免此类问题。希望这篇文章能帮助你在开发过程中顺利解决问题。




</content:encoded></item><item><title>error trying to exec &apos;cc1&apos; execvp No such file or directory错误的解决办法</title><link>https://www.jason-z.com/posts/error-trying-to-exec-cc1-execvp-no-such-file-or-directory/</link><guid isPermaLink="true">https://www.jason-z.com/posts/error-trying-to-exec-cc1-execvp-no-such-file-or-directory/</guid><pubDate>Wed, 13 Sep 2023 08:25:18 GMT</pubDate><content:encoded>
## 概述

在升级 GCC 版本后，使用 `node-gyp` 安装某些依赖时可能会遇到以下错误提示：

```
error trying to exec &apos;cc1&apos;: execvp: No such file or directory
```

该错误表示系统无法找到 `cc1` 编译器组件，导致编译失败。本文将介绍如何解决这一问题。

## 问题原因

`cc1` 是 GCC 编译器的一个内部组件，通常位于 GCC 安装目录中。如果升级 GCC 后未正确安装或配置 `cc1`，则会导致上述错误。

## 解决办法

### 方法一：确保 GCC 完整安装

1. **检查 GCC 安装**：
   - 确认 GCC 是否已正确安装，并且所有必要的组件都已包含在内。
   
   ```bash
   which gcc
   gcc --version
   ```

2. **重新安装 GCC**（如果需要）：
   - 如果发现 GCC 安装不完整或有问题，建议重新安装最新版本的 GCC。

   ```bash
   sudo yum install gcc  # CentOS/RHEL
   sudo apt-get install gcc  # Ubuntu/Debian
   ```

### 方法二：手动复制 `cc1` 文件

如果确认 GCC 已正确安装但仍然缺少 `cc1` 文件，可以尝试从 GCC 源码目录中手动复制 `cc1` 文件到 `/usr/bin` 目录。

1. **找到 `cc1` 文件路径**：
   - 进入 GCC 源码编译目录，找到编译好的 `cc1` 文件。

   ```bash
   cd /path/to/gcc-source/host-x86_64-pc-linux-gnu/stage1-gcc/
   ```

2. **复制 `cc1` 文件到 `/usr/bin`**：
   - 将 `cc1` 文件复制到 `/usr/bin` 目录下。

   ```bash
   sudo cp cc1 /usr/bin/
   ```

3. **验证 `cc1` 文件存在**：
   - 确保 `cc1` 文件已成功复制并可执行。

   ```bash
   ls -l /usr/bin/cc1
   ```

### 方法三：更新环境变量

有时，环境变量配置不正确也会导致找不到 `cc1` 文件。确保 `PATH` 环境变量中包含 GCC 安装路径。

1. **编辑环境变量文件**：
   - 打开 `~/.bashrc` 或 `/etc/profile` 文件，添加 GCC 安装路径。

   ```bash
   export PATH=/usr/local/gcc/bin:$PATH
   ```

2. **使更改生效**：
   - 重新加载环境变量配置文件。

   ```bash
   source ~/.bashrc
   ```

## 注意事项

- **备份重要文件**：在修改系统文件或路径之前，建议先备份重要文件，以防出现问题时可以恢复。
- **权限管理**：确保你有足够的权限来执行相关命令，必要时使用 `sudo` 提升权限。
- **依赖项检查**：确保所有依赖项均已正确安装，特别是与编译工具链相关的包。


</content:encoded></item><item><title>Strapi无法使用pnpm构建</title><link>https://www.jason-z.com/posts/strapi-unable-to-build-with-pnpm/</link><guid isPermaLink="true">https://www.jason-z.com/posts/strapi-unable-to-build-with-pnpm/</guid><pubDate>Wed, 13 Sep 2023 08:12:21 GMT</pubDate><content:encoded>
## 简介

在使用 **pnpm** 对 **Strapi** 进行构建时，可能会遇到类似以下的错误提示：

```
Can&apos;t resolve &apos;lodash/merge&apos; in &apos;/var/www/html/application/apps/cms/.cache/admin/src&apos;
```

## 问题原因与解决方案

### 官方 Issue

该问题已经在 [Strapi 官方 GitHub](https://github.com/strapi/strapi/issues/15992) 上有相关讨论。建议参考其中的解决方案进行尝试。

### 解决方案

根据社区反馈，以下是几种可能的解决方法：

#### 方法一：清理缓存并重新安装依赖

1. 清理 pnpm 缓存：
   ```bash
   pnpm store prune
   ```

2. 删除 `node_modules` 和 `pnpm-lock.yaml` 文件：
   ```bash
   rm -rf node_modules pnpm-lock.yaml
   ```

3. 重新安装依赖：
   ```bash
   pnpm install
   ```

#### 方法二：使用 Yarn 或 npm

如果上述方法无效，可以考虑切换到 **Yarn** 或 **npm** 来进行构建。这两种工具在大多数情况下能正常工作：

```bash
# 使用 Yarn
yarn install

# 或者使用 npm
npm install
```



</content:encoded></item><item><title>Next.js13 添加百度统计代码</title><link>https://www.jason-z.com/posts/nextjs-13-add-baidu-analytics-code/</link><guid isPermaLink="true">https://www.jason-z.com/posts/nextjs-13-add-baidu-analytics-code/</guid><pubDate>Wed, 13 Sep 2023 07:36:56 GMT</pubDate><content:encoded>

## 概述

在 Next.js 13 项目中添加百度统计代码，可以通过 `dangerouslySetInnerHTML` 属性将统计脚本插入到页面中。本文将详细介绍如何实现这一操作。

## 实现步骤

### 1. 创建 `RootLayout` 组件

在 Next.js 13 中，`RootLayout` 组件是应用的根布局组件。我们将在该组件中添加百度统计代码。

### 2. 添加百度统计代码

使用 `dangerouslySetInnerHTML` 属性将百度统计的 JavaScript 代码插入到页面中。

#### 示例代码

```jsx
import { Inter } from &apos;next/font/google&apos;;

const inter = Inter({ subsets: [&apos;latin&apos;] });

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const getBdAnalyticsTag = () =&gt; {
    return {
      __html: `
        var _hmt = _hmt || [];
        (function() {
            var hm = document.createElement(&quot;script&quot;);
            hm.src = &quot;https://hm.baidu.com/hm.js?9df80c64395dc22f25b7879090db0805&quot;;
            var s = document.getElementsByTagName(&quot;script&quot;)[0];
            s.parentNode.insertBefore(hm, s);
        })();
      `,
    };
  };

  return (
    &lt;html lang=&quot;en&quot;&gt;
      &lt;body className={inter.className}&gt;
        &lt;script dangerouslySetInnerHTML={getBdAnalyticsTag()} /&gt;
        {children}
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
```

### 详细说明

1. **导入字体**：
   - 使用 `next/font/google` 导入所需的字体。

2. **定义 `getBdAnalyticsTag` 方法**：
   - 该方法返回一个对象，其中 `__html` 属性包含百度统计的 JavaScript 代码。

3. **插入脚本**：
   - 使用 `dangerouslySetInnerHTML` 属性将生成的 HTML 字符串插入到 `&lt;script&gt;` 标签中。

4. **返回布局结构**：
   - 在 `&lt;body&gt;` 标签中插入百度统计脚本，并渲染子组件 `{children}`。


    


</content:encoded></item><item><title>2023.8.16 梦</title><link>https://www.jason-z.com/posts/dream-2023-08-16/</link><guid isPermaLink="true">https://www.jason-z.com/posts/dream-2023-08-16/</guid><pubDate>Thu, 17 Aug 2023 06:26:00 GMT</pubDate><content:encoded>


昨天做梦，又梦见了老爸。

距离上次梦到他不到半个月。

梦里面又是回到了老家的房子里，好像也是冬天，老爸躺着房间里的床上，家里的那个烤灯还依旧开着。

梦里面的他也是情况很不好了，也就是即将进入弥留之际了，大家都在家里等着。

这个时候，村里另外一个女孩ZZ过来了（和我们差不多同龄的，真实姓名就不说了），她说要到我们家玩，很开心的样子，

老爸突然就坐起来了，然后就似乎好像病完全好了一样（应该就是回光返照吧），然后他就坐在我们家里的那个客厅的沙发上，

和那个女孩，一起聊了很多（具体聊什么，记不清楚了），他那个神情和姿态和他之前一样，眉飞色舞的，然后就让人似乎忘了他生病一样。

后来不知道过了多久，那个女孩走了，然后家里又恢复了平静，老爸又躺了回去。

我也和老爸睡在一个床上，那个烤灯依旧开着，这个时候，我三叔突然走进了房间，坐在床旁边的凳子上，

他的神情很凝重，然后就跟我老爸说了一句： ”你真的不埋回去吗？？“

老爸没有说话，我觉得气氛很尴尬，

于是，就和我三叔说了我的想法，我说三叔：”我爸想呆在这里，就先让他呆这里吧，以后，如果像ZCL(我儿子)的下一代如果不在都江堰发展的话，我就让ZCL把我和我爸都送回来....“

旁边的烤灯也慢慢变得昏暗，

老爸依旧没有说话，但是因为我和他睡在一个被子里，我能逐渐感受到他渐渐变的冰凉。。。

他又一次在梦里离开了我。


</content:encoded></item><item><title>Centos源码编译安装ImageMagick7</title><link>https://www.jason-z.com/posts/centos-source-code-install-imagemagick-7/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos-source-code-install-imagemagick-7/</guid><pubDate>Sun, 05 Feb 2023 23:40:27 GMT</pubDate><content:encoded>
* 1、安装依赖
```shell
# yum groupinstall &apos;Development Tools&apos;
# yum -y install bzip2-devel freetype-devel libjpeg-devel libpng-devel libtiff-devel giflib-devel zlib-devel ghostscript-devel djvulibre-devel libwmf-devel jasper-devel libtool-ltdl-devel libX11-devel libXext-devel libXt-devel lcms-devel libxml2-devel librsvg2-devel OpenEXR-devel php-devel
```
* 2、下载安装包

```shell   
# wget https://www.imagemagick.org/download/ImageMagick.tar.gz
# tar xvzf ImageMagick.tar.gz
```

* 3、编译安装

```shell   
# cd ImageMagick*
# ./configure
# make
# make install
```

* 4、验证

```shell     
# magick -version

Version: ImageMagick 7.0.8-28 Q16 x86_64 2019-02-19 https://imagemagick.org
Copyright: (C) 1999-2019 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenMP 
Delegates (built-in): bzlib djvu fontconfig freetype jng jpeg lzma openexr pangocairo png tiff wmf x xml zlib
```

</content:encoded></item><item><title>Centos7编译安装GraphicsMagick</title><link>https://www.jason-z.com/posts/centos-source-install-graphicsmagick/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos-source-install-graphicsmagick/</guid><pubDate>Sun, 05 Feb 2023 23:27:34 GMT</pubDate><content:encoded>* 1、安装依赖

```shell
yum install -y gcc libpng libjpeg libpng-devel libjpeg-devel ghostscript libtiff libtiff-devel freetype freetype-devel 
```

* 2、下载最新安装包

&lt;https://sourceforge.net/projects/graphicsmagick/files/&gt;

* 3、解压

```shell     
xz -d GraphicsMagick-1.3.40.xz
tar -zxvf GraphicsMagick-1.3.40.tar
```

* 4、编译安装

    
```shell   
cd graphicsMagick-1.3.40 
./configure
make
make install 
```

* 5、验证是否安装成功
 
 ```shell    
gm version
gm convert -list formats 
```

</content:encoded></item><item><title>ERR! sharp EACCES permission denied, mkdir ‘/root/.npm/_libvips&apos;错误的解决方案</title><link>https://www.jason-z.com/posts/npm-sharp-eaccess-permission-denied-error/</link><guid isPermaLink="true">https://www.jason-z.com/posts/npm-sharp-eaccess-permission-denied-error/</guid><pubDate>Sun, 05 Feb 2023 23:19:33 GMT</pubDate><content:encoded>

在使用 NPM 安装包时，有时会遇到以下错误提示：

```
sharp@0.31.3 install /root/.nvm/versions/node/v14.17.0/lib/node_modules/favicons/node_modules/sharp

&gt; (node install/libvips &amp;&amp; node install/dll-copy &amp;&amp; prebuild-install) || (node install/can-compile &amp;&amp; node-gyp rebuild &amp;&amp; node install/dll-copy)

sharp: Installation error: EACCES: permission denied, mkdir &apos;/root/.npm/_libvips&apos;

sharp: Are you trying to install as a root or sudo user?

sharp: - For npm &lt;= v6, try again with the &quot;--unsafe-perm&quot; flag
sharp: - For npm &gt;= v8, the user must own the directory &quot;npm install&quot; is run in
sharp: Please see https://sharp.pixelplumbing.com/install for required dependencies
```

## 问题分析

根据错误提示，这通常是权限问题。即使当前使用的是 root 账户运行，也会出现此错误。原因可能是 NPM 需要一个专门用于运行 NPM 的高权限账户，或者需要调整安装目录的权限。

## 解决方法

### 方法一：使用 `--unsafe-perm` 选项

对于 npm 版本 &lt;= v6，可以尝试使用 `--unsafe-perm` 选项来忽略权限检查：

```shell
npm install &lt;package-name&gt; -g --unsafe-perm
```

### 方法二：确保安装目录权限正确

对于 npm 版本 &gt;= v8，确保当前用户拥有运行 `npm install` 的目录权限。可以通过以下步骤解决：

1. **切换到非 root 用户**：尽量避免以 root 用户身份运行 NPM 安装命令。
2. **更改 NPM 全局安装路径**：将 NPM 的全局安装路径更改为当前用户的家目录下。

   ```shell
   npm config set prefix ~/.npm-global
   ```

   然后将 `~/.npm-global/bin` 添加到系统的 PATH 环境变量中：

   ```shell
   export PATH=~/.npm-global/bin:$PATH
   ```

3. **重新安装包**：

   ```shell
   npm install &lt;package-name&gt; -g
   ```

### 方法三：使用 nvm 管理 Node.js 和 NPM

如果你使用 `nvm` 来管理 Node.js 和 NPM，建议确保当前使用的 Node.js 版本和 NPM 版本是最新的，并且以普通用户身份运行安装命令。




</content:encoded></item><item><title>MacOS关闭brew install 自动更新</title><link>https://www.jason-z.com/posts/macos-disable-brew-install-autoupdate/</link><guid isPermaLink="true">https://www.jason-z.com/posts/macos-disable-brew-install-autoupdate/</guid><pubDate>Sat, 04 Feb 2023 18:57:18 GMT</pubDate><content:encoded>

在 MacOS 系统上，默认情况下使用 `brew install` 命令安装程序时，Homebrew 会自动对所有软件进行更新。如果我们只想单独安装软件而不需要全局更新，可以通过以下方法关闭自动更新功能。

## 临时关闭自动更新

在执行 `brew install` 命令前，添加环境变量 `HOMEBREW_NO_AUTO_UPDATE=1`，例如：

```shell
HOMEBREW_NO_AUTO_UPDATE=1 brew install &lt;package_name&gt;
```

## 永久关闭自动更新

如果需要一直关闭此功能，可以通过配置环境变量来实现。

### Bash

编辑 `~/.bashrc` 文件并添加环境变量：

```shell
echo &apos;export HOMEBREW_NO_AUTO_UPDATE=1&apos; &gt;&gt; ~/.bashrc
source ~/.bashrc
```

### Zsh

编辑 `~/.zshrc` 文件并添加环境变量：

```shell
echo &apos;export HOMEBREW_NO_AUTO_UPDATE=1&apos; &gt;&gt; ~/.zshrc
source ~/.zshrc
```


</content:encoded></item><item><title>Android Studio中gradle.properties中文乱码解决方案</title><link>https://www.jason-z.com/posts/android-studio-gradle-properties-chinese-garbled/</link><guid isPermaLink="true">https://www.jason-z.com/posts/android-studio-gradle-properties-chinese-garbled/</guid><pubDate>Sat, 04 Feb 2023 03:10:44 GMT</pubDate><content:encoded>
## 问题描述

如果在Android Studio的gradle.properties中定义中文后，那么编译打包后生成的XML文件中会出现乱码，

例如在gradle.properties中定义

&gt;    appName=中文

build.gradle中定义

&gt;    resValue &quot;string&quot;, &quot;app_name&quot;, app_name

那么在生成的GradleRes.xml文件中的app_name会出现乱码

    
```xml    
    &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
    &lt;resources&gt;
    
        &lt;!-- Automatically generated file. DO NOT MODIFY --&gt;
    
        &lt;!-- Value from default config. --&gt;
        &lt;string name=&quot;app_name&quot; translatable=&quot;false&quot;&gt;？？？&lt;/string&gt;
    
    &lt;/resources&gt;
```


## 解决方案

使用过转换文字的编码，但是没有效果，可能和文件本身的编码也有关系

    
&gt; resValue &quot;string&quot;, &quot;app_name&quot;, new String(app_name.getBytes(&quot;iso8859-1&quot;), &quot;UTF-8&quot;) 

后来使用的方案使用URLENCODE进行过渡转换。

先把中文名称使用在线工具(&lt;https://www.toolkk.com/tools/url-encode-decode&gt;)进行URLENCODE


![](../attachments/tinymce/4b58ed95fefc71ca92feebe89b46ead063de3c9b393bb.png)

然后再gradle.proterties里定义

&gt;    appName=%E4%B8%AD%E6%96%87

修改app\build.gradle里的

    
&gt; resValue &quot;string&quot;, &quot;app_name&quot;, URLDecoder.decode(app_name.toString(), &quot;UTF-8&quot;)

然后，重新编译即可。


</content:encoded></item><item><title>There is no current event loop 错误的解决方法</title><link>https://www.jason-z.com/posts/deprecationwarning-there-is-no-current-event-loop/</link><guid isPermaLink="true">https://www.jason-z.com/posts/deprecationwarning-there-is-no-current-event-loop/</guid><pubDate>Fri, 23 Dec 2022 04:31:44 GMT</pubDate><content:encoded>
在 Python 中使用协程时，高版本的 Python 可能会报以下错误：

```
DeprecationWarning: There is no current event loop
```

## 解决方法

将原来的代码：

```python
loop = asyncio.get_event_loop()
```

改为：

```python
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
```

### 完整示例

为了确保代码在不同环境中都能正常运行，建议使用 `try-except` 块来处理不同的 Python 版本和环境：

```python
import asyncio

try:
    # 对于 Python 3.10 及以上版本
    loop = asyncio.get_running_loop()
except RuntimeError:
    # 如果没有正在运行的事件循环，则创建一个新的
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)

# 继续使用 loop 进行协程操作
```

</content:encoded></item><item><title>pycdc安装</title><link>https://www.jason-z.com/posts/pycdc-install/</link><guid isPermaLink="true">https://www.jason-z.com/posts/pycdc-install/</guid><pubDate>Wed, 21 Dec 2022 22:28:03 GMT</pubDate><content:encoded>

## 安装步骤

以下是安装 `pycdc` 的详细步骤：

### 1. 克隆仓库

首先，克隆 `pycdc` 的 GitHub 仓库：

```shell
git clone https://github.com/zrax/pycdc
```

### 2. 进入项目目录

进入克隆下来的项目目录：

```shell
cd pycdc
```

### 3. 配置构建环境

使用 `cmake` 配置构建环境：

```shell
cmake .
```

### 4. 编译

编译项目：

```shell
make
```

### 5. 运行测试

运行测试以确保一切正常：

```shell
make check
```

### 6. 使用 pycdc

使用 `pycdc` 解析 `.pyc` 文件。将 `/path/your.pyc` 替换为你要解析的 `.pyc` 文件路径：

```shell
python pycdc /path/your.pyc
```

</content:encoded></item><item><title>CXXABI_1.3.8 not found 错误的解决方案</title><link>https://www.jason-z.com/posts/cxxabi-138-not-found/</link><guid isPermaLink="true">https://www.jason-z.com/posts/cxxabi-138-not-found/</guid><pubDate>Mon, 19 Dec 2022 06:43:08 GMT</pubDate><content:encoded>


升级 Python 版本后，运行库时可能会遇到以下错误：

```
ImportError: /lib64/libstdc++.so.6: version `CXXABI_1.3.8&apos; not found (required by /usr/local/python3/lib/python3.7/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so)
```

## 解决方法

### 1. 查看本机情况

检查当前系统中已有的 CXXABI 版本：

```shell
strings /lib64/libstdc++.so.6 | grep CXXABI
```

如果输出不包含 `CXXABI_1.3.8`，则需要更新或替换 `libstdc++.so.6`。

### 2. 查找动态库位置

查找系统中所有 `libstdc++.so*` 文件的位置：

```shell
find / -name &quot;libstdc++.so*&quot; 2&gt;/dev/null
```

### 3. 验证目标动态库

找到一个包含 `CXXABI_1.3.8` 的 `libstdc++.so.6` 文件，并验证其版本：

```shell
strings /var/lib/docker/overlay2/83ab3664426d4266880c6cf241eca1dd14364fd91892f11a4ca58c31c46c4050/diff/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25 | grep CXXABI
```

确保输出包含 `CXXABI_1.3.8`。

### 4. 复制动态库

将包含 `CXXABI_1.3.8` 的 `libstdc++.so.6` 文件复制到 `/usr/lib64/` 目录：

```shell
sudo cp /var/lib/docker/overlay2/83ab3664426d4266880c6cf241eca1dd14364fd91892f11a4ca58c31c46c4050/diff/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25 /usr/lib64/
```

### 5. 建立软连接

移除旧的符号链接并创建新的符号链接：

```shell
sudo rm /usr/lib64/libstdc++.so.6
sudo ln -s /usr/lib64/libstdc++.so.6.0.25 /usr/lib64/libstdc++.so.6
```
</content:encoded></item><item><title>Centos7安装Opencc</title><link>https://www.jason-z.com/posts/centos7-install-opencc/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos7-install-opencc/</guid><pubDate>Mon, 19 Dec 2022 06:25:51 GMT</pubDate><content:encoded>

## 准备工作

安装必要的依赖包：

```shell
yum -y install cmake
yum -y install git
```

## 下载代码

克隆 OpenCC 仓库：

```shell
git clone https://github.com/BYVoid/OpenCC
```

## 安装文档生成工具

安装 Doxygen 用于生成 OpenCC 的使用文档：

```shell
yum -y install doxygen
```

## 编译安装

进入 OpenCC 目录并编译安装：

```shell
cd OpenCC
mkdir build &amp;&amp; cd build
cmake ..
make &amp;&amp; sudo make install
```

## 创建链接

创建符号链接以确保库文件路径正确：

```shell
ln -s /usr/lib/libopencc.so.2 /usr/lib64/libopencc.so.2
```

</content:encoded></item><item><title>Centos7安装GCC9.3</title><link>https://www.jason-z.com/posts/centos7-install-gcc93/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos7-install-gcc93/</guid><pubDate>Mon, 19 Dec 2022 06:14:48 GMT</pubDate><content:encoded>

高版本的 GCC 需要借助 **devtoolset** 来安装。

## 安装步骤

### 1. 安装 SCL

首先，安装 Software Collections (SCL)：

```shell
yum -y install centos-release-scl-rh
```

### 2. 安装 devtoolset

接着，安装 devtoolset-9：

```shell
yum -y install devtoolset-9*
```

### 3. 启动 devtoolset

启动 devtoolset-9 并使其生效：

```shell
scl enable devtoolset-9 bash
source /opt/rh/devtoolset-9/enable
```

### 4. 加入到开机启动

为了使 devtoolset-9 在系统启动时自动启用，可以在 `/etc/rc.d/rc.local` 文件最下面加入以下内容：

```shell
echo &quot;source /opt/rh/devtoolset-9/enable&quot; &gt;&gt; /etc/rc.d/rc.local
chmod +x /etc/rc.d/rc.local
```

### 5. 验证版本

验证 GCC 是否成功安装并生效：

```shell
which gcc
gcc --version
```

## 移除 devtoolset

如果需要移除 devtoolset-9，可以使用以下命令：

```shell
yum -y remove devtoolset-9* libasan libatomic libcilkrts libitm liblsan libtsan libubsan
```


通过以上步骤，你应该能够在 CentOS 7 上成功安装并配置 GCC 9.3。如果遇到任何问题，请参考官方文档或社区支持。

</content:encoded></item><item><title>Centos7安装GCC5.2</title><link>https://www.jason-z.com/posts/centos7-install-gcc52/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos7-install-gcc52/</guid><pubDate>Fri, 16 Dec 2022 03:43:56 GMT</pubDate><content:encoded>

## 简介

CentOS 7 默认的 GCC 版本较低，如果需要升级到 GCC 5.2，可以通过源码编译的方式进行安装。

## 步骤

### 1. 下载 GCC 5.2.0 源码

使用 `wget` 命令下载 GCC 5.2.0 的源码包：

```shell
wget http://ftp.gnu.org/gnu/gcc/gcc-5.2.0/gcc-5.2.0.tar.bz2
tar -jxvf gcc-5.2.0.tar.bz2
```

### 2. 进入 GCC 目录

解压完成后，进入 GCC 源码目录：

```shell
cd gcc-5.2.0
```

### 3. 下载依赖包

GCC 需要一些额外的依赖包，可以使用自带的脚本下载：

```shell
./contrib/download_prerequisites
```

### 4. 创建 build 文件夹

为了避免污染源码目录，建议在源码目录外创建一个 `build` 文件夹进行编译：

```shell
mkdir build
cd build
```

### 5. 编译安装

配置并编译 GCC。此过程可能耗时较长，请耐心等待：

```shell
../configure --prefix=/usr/local/gcc --enable-languages=c,c++ --disable-multilib
make &amp;&amp; sudo make install
```

### 6. 修改软连接

为确保系统默认使用新安装的 GCC 和 G++，需要修改软连接：

```shell
sudo mv /usr/bin/gcc /usr/bin/gcc_bak
sudo ln -s /usr/local/gcc/bin/gcc /usr/bin/gcc

sudo mv /usr/bin/g++ /usr/bin/g++_bak
sudo ln -s /usr/local/gcc/bin/g++ /usr/bin/g++
```

### 7. 查看升级后版本

最后，验证 GCC 和 G++ 是否成功升级：

```shell
gcc --version
g++ --version
```


通过以上步骤，你应该能够在 CentOS 7 上成功安装并配置 GCC 5.2。如果遇到任何问题，请参考官方文档或社区支持。

</content:encoded></item><item><title>Centos7安装Cmake3.5</title><link>https://www.jason-z.com/posts/centos7-install-cmake35/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos7-install-cmake35/</guid><pubDate>Fri, 16 Dec 2022 03:23:32 GMT</pubDate><content:encoded>

## 简介

本文档介绍如何在 Cen././////////////////////////////////tOS 7 上安装 CMake 3.5。

## 步骤

### 1. 下载 CMake 3.5

使用 `wget` 命令下载 CMake 3.5 的源码包：

```shell
wget https://cmake.org/files/v3.5/cmake-3.5.2.tar.gz
```

### 2. 解压文件

解压下载的压缩包：

```shell
tar xvf cmake-3.5.2.tar.gz
```

### 3. 编译

进入解压后的目录并进行编译：

```shell
cd cmake-3.5.2
./bootstrap --prefix=/usr
```

&gt; **注意**: 确保你进入了正确的目录（`cmake-3.5.2`），而不是 `cmake-3.4.3`。

### 4. 安装

编译完成后，执行安装命令：

```shell
make &amp;&amp; sudo make install
```

### 5. 验证版本

最后，验证 CMake 是否安装成功并检查其版本：

```shell
cmake --version
```

</content:encoded></item><item><title>select2实现拖动排序</title><link>https://www.jason-z.com/posts/select2-drop-sortable/</link><guid isPermaLink="true">https://www.jason-z.com/posts/select2-drop-sortable/</guid><pubDate>Sun, 04 Dec 2022 00:07:03 GMT</pubDate><content:encoded>

如果要在 Select2 多选情况下实现项目可以拖动排序，可以参考以下代码：

## 示例代码

```javascript
var selectEl = $(&apos;select&apos;).select2();

// 使 Select2 的选项可拖动排序
selectEl.next().children().children().children().sortable({
    containment: &apos;parent&apos;,
    stop: function (event, ui) {
        // 更新选项顺序
        ui.item.parent().children(&apos;[title]&apos;).each(function () {
            var title = $(this).attr(&apos;title&apos;);
            var original = $(&apos;option:contains(&apos; + title + &apos;)&apos;, selectEl).first();
            original.detach();
            selectEl.append(original);
        });
        selectEl.trigger(&apos;change&apos;);
    }
});
```

## 代码说明

1. **初始化 Select2**：
   ```javascript
   var selectEl = $(&apos;select&apos;).select2();
   ```

2. **使 Select2 的选项可拖动排序**：
   使用 jQuery UI 的 `sortable` 方法来使 Select2 的选项可拖动。注意选择器的选择路径可能因 Select2 版本不同而有所变化。

3. **更新选项顺序**：
   在拖动结束时（`stop` 事件），遍历所有带有 `title` 属性的元素，并根据其标题找到对应的 `&lt;option&gt;` 元素，重新排列它们的顺序并触发 `change` 事件以确保 Select2 组件更新状态。

4. **触发 `change` 事件**：
   ```javascript
   selectEl.trigger(&apos;change&apos;);
   ```

## 注意事项

- 确保已引入 jQuery 和 jQuery UI 库。
- 根据 Select2 的版本和结构调整选择器路径。
- 如果使用的是较新的 Select2 版本，建议查阅官方文档以获取最新的选择器路径。
</content:encoded></item><item><title>Dropzone只允许上传一个文件</title><link>https://www.jason-z.com/posts/dropzone-only-file-select/</link><guid isPermaLink="true">https://www.jason-z.com/posts/dropzone-only-file-select/</guid><pubDate>Sat, 03 Dec 2022 23:52:19 GMT</pubDate><content:encoded>

在使用 Dropzone 时，即使设置了 `maxFiles: 1` 和 `uploadMultiple: false`，用户在界面上仍然可以选择多个文件（虽然会有错误提示）。为了确保界面也只允许选择一个文件，可以通过事件监听来实现。

## 解决方法

通过监听 `maxfilesexceeded` 事件，并在初始化时设置相关配置：

```javascript
Dropzone.options.myDropzone = {
    uploadMultiple: false,
    maxFiles: 1,
    init: function() {
        this.on(&quot;maxfilesexceeded&quot;, function(file) {
            alert(&quot;每次仅允许选择一个文件&quot;);
            this.removeFile(file);
        });
    }
};
```

### 完整示例

为了确保更好的用户体验，可以进一步优化代码，确保用户在选择文件时不会出现混淆：

```javascript
Dropzone.options.myDropzone = {
    uploadMultiple: false,
    maxFiles: 1,
    init: function() {
        this.on(&quot;addedfile&quot;, function(file) {
            if (this.files.length &gt; 1) {
                alert(&quot;每次仅允许选择一个文件&quot;);
                this.removeFile(this.files[0]);
            }
        });

        this.on(&quot;maxfilesexceeded&quot;, function(file) {
            alert(&quot;每次仅允许选择一个文件&quot;);
            this.removeFile(file);
        });
    }
};
```




</content:encoded></item><item><title>CSS网站变灰效果</title><link>https://www.jason-z.com/posts/css-website-gray-effect/</link><guid isPermaLink="true">https://www.jason-z.com/posts/css-website-gray-effect/</guid><pubDate>Thu, 01 Dec 2022 04:21:25 GMT</pubDate><content:encoded>
配合一些哀悼日，需要把网站变为灰色，这里可以借助`css3`的`filter`属性来实现。

代码如下:

```css
html {
    -webkit-filter : grayscale(100%);
    -moz-filter: grayscale(100%);
    -o-filter: grayscale(100%);
    filter: grayscale(100%);
  filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1);
}
```

</content:encoded></item><item><title>jQuery Ajax上传文件</title><link>https://www.jason-z.com/posts/jquery-ajax-file-upload/</link><guid isPermaLink="true">https://www.jason-z.com/posts/jquery-ajax-file-upload/</guid><pubDate>Tue, 29 Nov 2022 00:27:05 GMT</pubDate><content:encoded>

通过 `FormData` 手动构造字段，并配合 `ajax` 的 `processData` 和 `contentType` 选项，可以实现文件上传。示例代码如下：

```javascript
var formData = new FormData();
formData.append(&apos;file&apos;, $(&apos;#file&apos;)[0].files[0]);

$.ajax({
    url: &apos;upload.php&apos;,
    type: &apos;POST&apos;,
    data: formData,
    processData: false,  // 防止 jQuery 自动转换数据为查询字符串
    contentType: false,  // 不设置内容类型，允许浏览器根据数据自动设置
    success: function(data) {
        console.log(data);
        alert(data);
    },
    error: function(xhr, status, error) {
        console.error(status, error);
    }
});
```


</content:encoded></item><item><title>jimp 转换gif格式错误的解决办法</title><link>https://www.jason-z.com/posts/jimp-gif-convert-error/</link><guid isPermaLink="true">https://www.jason-z.com/posts/jimp-gif-convert-error/</guid><pubDate>Mon, 28 Nov 2022 06:09:31 GMT</pubDate><content:encoded>
## 问题描述

在使用 Jimp 的 `getBase64Async` 函数将图片转换为 GIF 格式时，得到的 base64 编码结果为：

```
data:image/gif;base64,[object Promise]
```

这个问题在 [Jimp GitHub Issue #1056](https://github.com/oliver-moran/jimp/issues/1056) 中有详细讨论。

## 解决办法

使用 `getBufferAsync` 方法代替 `getBase64Async`，并手动构建 base64 字符串：

```javascript
image.getBufferAsync(image.getMIME()).then(data =&gt; { 
    const base64 = &apos;data:&apos; + image.getMIME() + &apos;;base64,&apos; + data.toString(&apos;base64&apos;);
    // 处理生成的 base64 字符串
});
```
</content:encoded></item><item><title>Centos7 Yum安装PHP8.0</title><link>https://www.jason-z.com/posts/centos7-yum-install-php80/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos7-yum-install-php80/</guid><pubDate>Sun, 27 Nov 2022 08:37:43 GMT</pubDate><content:encoded>
## 安装步骤

### 1. 安装 REMI 源

安装 EPEL 和 REMI 源：

```shell
yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install https://rpms.remirepo.net/enterprise/remi-release-7.rpm
```

### 2. 安装 yum-utils

安装 `yum-utils` 工具：

```shell
yum -y install yum-utils
```

### 3. 开启 PHP 8.0 REMI 库

禁用所有 REMI PHP 库，并启用 PHP 8.0 的 REMI 库：

```shell
yum-config-manager --disable &apos;remi-php*&apos;
yum-config-manager --enable remi-php80
```

### 4. 更新 yum

更新系统包以确保使用最新的软件源信息：

```shell
yum update
```

### 5. 安装 PHP 8.0 及其扩展

安装 PHP 8.0 及常用扩展：

```shell
yum -y install php php-devel php-mysql
```

### 6. 验证版本

验证 PHP 是否成功安装并生效：

```shell
php -v
```

</content:encoded></item><item><title>MacOS安装Swoole提示library not found for -lssl解决方案</title><link>https://www.jason-z.com/posts/macos-swoole-install-library-lssl-not-found/</link><guid isPermaLink="true">https://www.jason-z.com/posts/macos-swoole-install-library-lssl-not-found/</guid><pubDate>Sun, 27 Nov 2022 08:31:59 GMT</pubDate><content:encoded>
## 问题描述

在 MacOS 上使用 Pecl 安装 Swoole 并开启 SSL 支持时，可能会遇到以下错误：

```
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [swoole.la] Error 1
ERROR: `make&apos; failed
```

## 问题分析

此问题通常是由于安装过程中未正确识别系统的 OpenSSL 路径导致的。

## 解决方法

### 1. 查看系统安装的 OpenSSL 版本

使用 Homebrew 列出已安装的 OpenSSL 公式：

```shell
brew list --formula | grep openssl
```

### 2. 查看 OpenSSL 的路径

获取 OpenSSL 的安装路径信息：

```shell
brew info openssl@1.1
```

### 3. 重新安装 Swoole

在安装 Swoole 时，当提示是否启用 SSL 支持时，输入 `yes` 并指定 OpenSSL 的路径。例如：

```shell
pecl install swoole
```

在安装过程中，当出现以下提示时：

```
enable openssl support? [no]
```

输入：

```
yes --with-openssl-dir=/usr/local/opt/openssl@1.1/
```

确保将 `/usr/local/opt/openssl@1.1/` 替换为你实际的 OpenSSL 安装路径。

</content:encoded></item><item><title>syntax error, unexpected $end, expecting &apos;]&apos; in /etc/php.d/php_browscap.ini 错误的解决方法</title><link>https://www.jason-z.com/posts/browscap-ini-syntax-error/</link><guid isPermaLink="true">https://www.jason-z.com/posts/browscap-ini-syntax-error/</guid><pubDate>Sun, 27 Nov 2022 08:19:57 GMT</pubDate><content:encoded>## 问题描述

下载 `php_browscap.ini` 文件到 `/etc/php.d/` 目录下之后，运行PHP会提示以下错误：

&gt; PHP:  syntax error, unexpected end of file, expecting &apos;]&apos; in
&gt; /etc/php.d/php_browscap.ini on line 44

## 问题解决

将下载的`php_browscap.ini` 文件放置到其他目录，例如`/etc/php_browscap.ini`目录下即可。

参考此[issue](https://github.com/browscap/browscap/issues/1842)


</content:encoded></item><item><title>Centos7安装Libreoffice</title><link>https://www.jason-z.com/posts/centos7-install-libreoffice/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos7-install-libreoffice/</guid><pubDate>Fri, 25 Nov 2022 01:10:55 GMT</pubDate><content:encoded>

# Centos7 安装 LibreOffice

## 安装步骤

### 1. 更新系统包

确保系统包是最新的：

```shell
sudo yum update -y
```

### 2. 安装 LibreOffice

使用 `yum` 安装 LibreOffice：

```shell
sudo yum install -y libreoffice
```

### 3. 验证安装

验证 LibreOffice 是否成功安装：

```shell
libreoffice --version
```

</content:encoded></item><item><title>Python3提示ModuleNotFoundError  No module named “_ctypes”的解决方法</title><link>https://www.jason-z.com/posts/python3-no-module-named-ctypes/</link><guid isPermaLink="true">https://www.jason-z.com/posts/python3-no-module-named-ctypes/</guid><pubDate>Fri, 25 Nov 2022 00:46:37 GMT</pubDate><content:encoded>

## 问题描述

在安装或使用 Python 3 时，可能会遇到以下错误提示：

```
import ctypes
File &quot;/usr/local/python3/lib/python3.7/ctypes/__init__.py&quot;, line 7, in &lt;module&gt;
    from _ctypes import Union, Structure, Array
ModuleNotFoundError: No module named &apos;_ctypes&apos;
```

## 问题分析

此问题通常是由于系统缺少外部链接库 `libffi` 导致的。`_ctypes` 模块依赖于 `libffi` 库来实现 C 类型的支持。

## 解决方案

### 安装依赖库并重新安装 Python 3

1. **安装 `libffi-devel` 依赖库**

   使用 `yum` 安装 `libffi-devel` 以确保系统中有必要的开发库：

   ```shell
   yum install libffi-devel -y
   ```

2. **重新编译和安装 Python 3**

   如果你是从源码编译安装的 Python 3，建议重新编译和安装 Python 3 以确保 `_ctypes` 模块正确生成：

   ```shell
   cd /path/to/python3-source
   ./configure
   make
   sudo make install
   ```

3. **验证安装**

   验证 `_ctypes` 模块是否已成功加载：

   ```python
   python3 -c &quot;import ctypes; print(ctypes.__file__)&quot;
   ```



</content:encoded></item><item><title>Centos7安装Python3.7</title><link>https://www.jason-z.com/posts/centos7-install-python37/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos7-install-python37/</guid><pubDate>Fri, 25 Nov 2022 00:30:12 GMT</pubDate><content:encoded>
文本讲述的是 Python 3.7 的源码编译安装方法。

## 安装步骤

### 1. 检查当前的 Python 和 GCC 版本

检查当前系统中已安装的 Python 和 GCC 版本：

```shell
python -V
gcc --version
```

如果提示 `gcc: command not found`，则需要安装 GCC：

```shell
yum -y install gcc
```

### 2. 安装依赖

安装编译 Python 所需的依赖包：

```shell
yum -y install gcc patch libffi-devel python-devel zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel
```

### 3. 下载、解压并安装

下载 Python 3.7.10 源码包并进行编译安装：

```shell
wget https://www.python.org/ftp/python/3.7.10/Python-3.7.10.tgz
tar zxvf Python-3.7.10.tgz
cd Python-3.7.10
./configure
make &amp;&amp; sudo make install
```

### 4. 建立软连接（如果需要的话）

为新安装的 Python 3.7 创建软连接，以便通过 `python3` 和 `pip3` 命令使用：

```shell
ln -s /usr/local/bin/python3.7 /usr/bin/python3
ln -s /usr/local/bin/pip3.7 /usr/bin/pip3
```

### 5. 验证版本

验证 Python 和 pip 是否成功安装：

```shell
python3 --version
pip3 --version
```

</content:encoded></item><item><title>Jquery如何获取DropZone实例</title><link>https://www.jason-z.com/posts/jquery-get-dropzone-instance/</link><guid isPermaLink="true">https://www.jason-z.com/posts/jquery-get-dropzone-instance/</guid><pubDate>Fri, 25 Nov 2022 00:07:32 GMT</pubDate><content:encoded>
在使用 Dropzone 时，有时需要通过 jQuery 获取 Dropzone 实例以调用其方法。以下是一个简单的示例，展示如何获取并操作 Dropzone 实例。

```javascript
// 初始化 Dropzone
var $dropZone = $(&quot;#mydropzone&quot;).dropzone({
    // Dropzone 配置选项
});

// 获取 Dropzone 实例并调用方法
var dropzoneInstance = $dropZone[0].dropzone;
dropzoneInstance.processQueue();
```

### 完整示例

为了确保代码的完整性和可读性，可以进一步优化代码结构，并添加必要的注释：

```javascript
// 初始化 Dropzone 并配置选项
$(&quot;#mydropzone&quot;).dropzone({
    url: &quot;/file/upload&quot;,  // 上传文件的目标 URL
    maxFiles: 1,          // 最大允许上传的文件数
    addRemoveLinks: true, // 显示移除链接
    init: function() {
        // 在初始化时绑定事件或其他逻辑
    }
});

// 获取 Dropzone 实例并调用 processQueue 方法
var $dropZone = $(&quot;#mydropzone&quot;);
if ($dropZone.length) {
    var dropzoneInstance = $dropZone[0].dropzone;
    if (dropzoneInstance) {
        dropzoneInstance.processQueue();  // 处理队列中的文件
    } else {
        console.error(&quot;Dropzone 实例未找到&quot;);
    }
}
```

</content:encoded></item><item><title>Centos7下使用Libreoffice中文乱码</title><link>https://www.jason-z.com/posts/centos7-libreoffice-chinese-garbled/</link><guid isPermaLink="true">https://www.jason-z.com/posts/centos7-libreoffice-chinese-garbled/</guid><pubDate>Thu, 24 Nov 2022 23:51:42 GMT</pubDate><content:encoded>

## 解决步骤

### 1. 验证系统是否安装中文字体

检查系统中已安装的字体：

```shell
fc-list
```

如果提示 `fc-list: command not found`，则需要安装相关依赖包：

```shell
yum -y install cups-libs fontconfig
```

### 2. 拷贝 Windows 字体

在 Windows 电脑上找到 `C:\Windows\Fonts` 目录，拷贝其中的 `.ttf` 字体文件。

![Windows 字体目录](https://upload-images.jianshu.io/upload_images/3535141-843177b49e88e1b4.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)

### 3. 将字体文件上传到 Linux 系统

在 Linux 系统的 `/usr/share/fonts` 目录下新建一个 `windows` 文件夹，并将字体文件上传到该文件夹，然后修改权限：

```shell
mkdir -p /usr/share/fonts/windows
chmod -R 755 /usr/share/fonts/windows
```

### 4. 刷新字体缓存

刷新字体缓存以使新字体生效：

```shell
fc-cache -fv
```

### 5. 验证字体是否加载成功

验证中文字体是否加载成功：

```shell
fc-list :lang=zh
```

如果第 4 步不生效，可以尝试重启机器：

```shell
reboot
```

</content:encoded></item><item><title>密码学学习笔记 - 频率分析</title><link>https://www.jason-z.com/posts/crypto-learning-notes-frequency-analysis/</link><guid isPermaLink="true">https://www.jason-z.com/posts/crypto-learning-notes-frequency-analysis/</guid><pubDate>Sun, 09 Oct 2022 06:26:00 GMT</pubDate><content:encoded>


频率分析是理论上是统计学中的一种方法，而应用在密码学历可以研究密文中字母或字母组的频率。并且可以做为作破解替换密码，凯撒密码，维吉尼亚密码的辅助工具。

## 原理

频率分析实际上的工作就是统计字母或者字母组合在文本中出现的次数，并通过和样本中的字母频率次数进行对比，然后找出对应的规律。

## 代码实现

我们可以借助于`python`的`Counter`方法来进行字母的统计。

```python
from collections import Counter
  
initializing string
test_str = &quot;THIS IS A TEST STRING&quot;
using collections.Counter() to get
count of each element in string
res = Counter(test_str)
printing result


print (&quot;Count of all characters is :\n &quot; +  str(res))
```

运行脚本，最终的运行结果是：

&gt; Count of all characters is :
&gt; Counter({&apos;T&apos;: 4, &apos;S&apos;: 4, &apos; &apos;: 4, &apos;I&apos;: 3, &apos;H&apos;: 1, &apos;A&apos;: 1, &apos;E&apos;: 1, &apos;R&apos;: 1, &apos;N&apos;: 1, &apos;G&apos;: 1})



当然，为了更直接的显示，我们可以借助 `Matplotlib` 这里图形绘制工具，将其绘制为图表。


```shell
python3 -m pip install matplotlib
```


然后，将上面的代码稍微修改下：


```python
from collections import Counter
import matplotlib.pyplot as plt
initializing string
test_str = &quot;THIS IS A TEST STRING&quot;
using collections.Counter() to get
count of each element in string
res = Counter(test_str).most_common()
x, y = zip(*res)
plt.bar(x, y)
plt.show()
```


重新运行，就可以生成可视化图表。



PS:

这里因为我的`python`能力有限，所以只做了一些简单的图表演示，实际上也可以参考[这篇文章](https://jrinconada.medium.com/cracking-caesar-cipher-8fe79226aabd)，把图表进一步丰富化。



另外一个，在实际进行频率分析的时候，我们可能不仅仅会对单字母分析，还可以对BIGRAMS（2个字母），TRIGRAMS（3个字母），甚至N-GRAM（多字母）的情况进行分析，同样，我可以修改代码可以达到这一目的。


```python
from collections import Counter
import matplotlib.pyplot as plt
initializing string
test_str = &quot;THIS IS A TEST STRING&quot;
Using Counter() + generator expression
res = Counter(test_str[idx : idx + 2] for idx in range(len(test_str) - 1))
printing result


print(&quot;The Bigrams Frequency is : &quot; + str(dict(res)))
```



运行结果

&gt; The Bigrams Frequency is : {&apos;TH&apos;: 1, &apos;HI&apos;: 1, &apos;IS&apos;: 2, &apos;S &apos;: 2, &apos; I&apos;: 1, &apos; A&apos;: 1, &apos;A &apos;: 1, &apos; T&apos;: 1, &apos;TE&apos;: 1, &apos;ES&apos;: 1, &apos;ST&apos;: 2, &apos;T &apos;: 1, &apos; S&apos;: 1, &apos;TR&apos;: 1, &apos;RI&apos;: 1, &apos;IN&apos;: 1, &apos;NG&apos;: 1}



当然，细心的同学也可能会发现，这里输出的双字母情况下还包含了空格的情况，所以实际应用中大家可以对上面的代码进行完善到达自己的目的。



这里，我自己也在工具匠上实现了一个在线的[频率分析工具](https://www.toolkk.com/tools/frequency-analysis)，大家有兴趣的也可以试试。



## 参考文献：

https://en.wikipedia.org/wiki/Frequency_analysis

https://jrinconada.medium.com/cracking-caesar-cipher-8fe79226aabd

https://realpython.com/python-counter/


</content:encoded></item><item><title>密码学学习笔记 - 维吉尼亚密码</title><link>https://www.jason-z.com/posts/crypto-learning-notes-vigenere-cipher/</link><guid isPermaLink="true">https://www.jason-z.com/posts/crypto-learning-notes-vigenere-cipher/</guid><pubDate>Sun, 09 Oct 2022 06:26:00 GMT</pubDate><content:encoded>


凯撒密码对于每个字母都会使用相同的偏移量进行偏移，从而造成了偏移量很有限（1-25），通过一些频率分析和穷举法就可以轻易破解，而维吉尼亚密码为了改进这一问题，使用了一系列的凯撒密码生成密码字母表（首次提出密钥的概念）来进行加密，我们也称之为多表密码。

我们还是先了解一下原理。

## 算法原理

首先，为了生成密码/密钥，我们需要借助表格法。这一表格包括了26行字母表，每一行都由前一行向左偏移一位得到，如图所示：

![](https://upload.wikimedia.org/wikipedia/commons/thumb/2/25/Vigen%C3%A8re_square.svg/300px-Vigen%C3%A8re_square.svg.png)

例如，假设明文为：

```
ATTACKATDAWN
```

我们随机选择某一关键词并重复而得到密钥，如关键词为LEMON时，密钥为：

```
LEMONLEMONLE
```

对于明文的第一个字母A，对应密钥的第一个字母L，于是使用表格中L行字母表进行加密，得到密文第一个字母L。类似地，明文第二个字母为T，在表格中使用对应的E行进行加密，得到密文第二个字母X。以此类推，可以得到：

明文：ATTACKATDAWN
密钥：LEMONLEMONLE
密文：LXFOPVEFRNHR

解密的过程则与加密相反。例如：根据密钥第一个字母L所对应的L行字母表，发现密文第一个字母L位于A列，因而明文第一个字母为A。密钥第二个字母E对应E行字母表，而密文第二个字母X位于此行T列，因而明文第二个字母为T。以此类推便可得到明文。

我们同样可以讲维吉尼亚密码的加密算法利用同余算法转换为数学公式。

## 代码实现：

* 加密

```python
import re
import string



alphabets = &quot;abcdefghijklmnopqrstuvwxyz&quot; # this is the english letters
def encrypt(p, k):
    c = &quot;&quot;
    kpos = [] # return the index of characters ex: if k=&apos;d&apos; then kpos= 3
    for x in k:
       # kpos += alphabets.find(x) #change the int value to string
        kpos.append(alphabets.find(x))
    i = 0
    for x in p:
      if i == len(kpos):
          i = 0
      pos = alphabets.find(x) + kpos[i] #find the number or index of the character and perform the shift with the key
      print(pos)
      if pos &gt; 25:
          pos = pos-26               # check you exceed the limit
      c += alphabets[pos].capitalize()  #because the cipher text always capital letters
      i +=1
    return c
```
* 解密：

```python
import re
import string


def decrypt(c, k):
    p = &quot;&quot;
    kpos = []
    for x in k:
        kpos.append(alphabets.find(x))
    i = 0
    for x in c:
      if i == len(kpos):
          i = 0
      pos = alphabets.find(x.lower()) - kpos[i]
      if pos &lt; 0:
          pos = pos + 26
      p += alphabets[pos].lower()
      i +=1
    return p
```


## 破解方法：

维吉尼亚密码因为引入了密钥的概念，所以可能密文相同的字母可能在明文中代表不同的明文，因此简单的统计字母出现的频率并不能破解。

但是这里大家再回头看一下算法原理的时候，会发现密钥生成规则时会发现，密钥的生成是通过关键词循环重复生成的，所以我们只要知道关键词的长度，就可以破解他。 而著名的卡西斯基试验和弗里德曼试验就是为了得到密钥的长度。



## 参考文献：

https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher



</content:encoded></item><item><title>密码学学习笔记 - 重合指数</title><link>https://www.jason-z.com/posts/crypto-learning-notes-index-coincidence/</link><guid isPermaLink="true">https://www.jason-z.com/posts/crypto-learning-notes-index-coincidence/</guid><pubDate>Sun, 09 Oct 2022 04:44:11 GMT</pubDate><content:encoded>
## 原理

重合指数（Index of Coincidence, IC），又称Friedman测试，是通过分析文本中相同字母出现的频率来破解多表密码（例如，维吉尼亚密码）的一种常用方法。

### 数学公式

重合指数的数学公式为：

\[
\mathbf{IC} = \frac{\sum_{i=1}^{c} n_i (n_i - 1)}{N(N-1)/c}
\]

其中：
- \( N \) 是文本字符的总长度，
- \( n_i \) 是字母（1-26）出现的次数。

对于英文语言，结合英文字母的频率，我们可以将公式换算为：

![重合指数公式](https://www.jason-z.com/storage/tinymce/images/4b58ed95fefc71ca92feebe89b46ead06343f2eb7293a.png)

其中 \( d \) 为密钥的长度。

### 应用场景

通过重合指数，我们能够区分密文是单表代换密码还是多表代换密码。具体来说：

- **单表代换密码**：如果密文的重合指数较高（接近 0.0700），类似于纯文本，则消息可能已使用转置密码或单字母替换加密。
- **多表代换密码**：如果密文的重合指数较低（接近 0.0385），类似于随机文本，则消息可能已使用多字母密码加密。

例如，密钥长度为 4-8 的维吉尼亚密码，IC 指数一般为 0.045 ± 0.05。

## 示例分析

考虑以下密文片段：

```
QPWKA LVRXC QZIKG RBPFA EOMFL JMSDZ VDHXC XJYEB IMTRQ WNMEA
IZRVK CVKVL XNEIC FZPZC ZZHKM LVZVZ IZRRQ WDKEC HOSNY XXLSP
MYKVQ XJTDC IOMEE XDQVS RXLRL KZHOV
```

我们分别带入密钥长度 1-10 来计算重合指数（这里计算的是集合）：

| 密钥长度 | IC 重合指数 |
|----------|-------------|
| 1        | 1.12        |
| 2        | 1.19        |
| 3        | 1.05        |
| 4        | 1.17        |
| 5        | 1.82        |
| 6        | 0.99        |
| 7        | 1.00        |
| 8        | 1.05        |
| 9        | 1.16        |
| 10       | 2.07        |

从上表可以看出，密钥长度为 5 和 10 时，IC 指数显著高于其他长度，这提示我们密钥长度可能是 5 或 10。

---

## 参考文献

- [Index of Coincidence - Wikipedia](https://en.wikipedia.org/wiki/Index_of_coincidence)
- [Kasiski Examination Assignment](https://github.com/sdsunjay/kasiski/blob/master/asgn2.pdf)

</content:encoded></item><item><title>密码学学习笔记 - 卡西斯基测试</title><link>https://www.jason-z.com/posts/crypto-learning-notes-kasiski-examination/</link><guid isPermaLink="true">https://www.jason-z.com/posts/crypto-learning-notes-kasiski-examination/</guid><pubDate>Sat, 08 Oct 2022 22:19:00 GMT</pubDate><content:encoded>
## 原理

卡西斯基测试（Kasiski Examination）是一种用于分析多表替换密码（如维吉尼亚密码）的方法。它通过寻找密文中重复出现的子串来推断密钥长度。

### 重复子串分析

由于一些较短的常用单词（例如英文单词 &quot;THE&quot;）在明文中频繁出现，它们极有可能被同样的密钥字母加密，从而在密文中重复出现。例如：

**明文**：
```
THE APPLE IS THE PEN
```

**密钥**：
```
ABCDEABCDEABCDEA
```

**密文**：
```
TIG DTPJF LW TIG SIN
```

可以看到，明文中的两个 &quot;THE&quot; 被加密成了相同的字符串 &quot;TIG&quot;。对于更长的段落，此方法更为有效，因为通常密文中重复的片段会更多。

### 示例分析

考虑以下密文片段：

```
DYDUXRMH TVDV NQD QNW DYDUXRMH ARTJGW NQD
```

其中，两个 &quot;DYDUXRMH&quot; 的出现相隔了 18 个字母。因此，可以假定密钥的长度是 18 的约数，即长度为 18、9、6、3 或 2。而两个 &quot;NQD&quot; 则相距 20 个字母，意味着密钥长度应为 20、10、5、4 或 2。取两者的交集，则可以基本确定密钥长度为 2。

---

## 代码实现

以下是卡西斯基测试的 Python 实现代码：

[源码链接](https://github.com/nytr0gen/kasiski/blob/master/kasiski.py)

```python
# https://github.com/sdsunjay/kasiski/blob/master/asgn2.pdf
# usage: python kasisky.py [ -v ] [ -m length ] [ infile [ outfile ] ]
# python kasiski.py krypton4.in | awk &apos;{print $4}&apos; | tail -n+3 | sort -nu | factor

from sys import argv
import re
import math

is_debug = False
debug = lambda *args, **kwargs: is_debug and print(*args, **kwargs)

def normalize(s):
    s = s.strip().upper()
    s = re.sub(r&apos;[^A-Z]+&apos;, &apos;&apos;, s)
    return s

def kasiski(s, min_num=3):
    s = normalize(s)
    out = &apos;&apos;

    matches = []
    found = {}
    for k in range(min_num, len(s) // 2):
        found[k] = {}
        shouldbreak = True
        for i in range(0, len(s) - k):
            v = s[i:i+k]
            if v not in found[k]:
                found[k][v] = 1
            else:
                found[k][v] += 1
                shouldbreak = False

        if shouldbreak:
            break

        for v in found[k]:
            if found[k][v] &gt; 2:
                matches.append(v)

    out += &quot;Length  Count  Word        Factor  Location (distance)\n&quot;
    out += &quot;======  =====  ==========  ======  ===================\n&quot;
    keylens = {}
    for v in matches:
        k = len(v)
        p = []
        for i in range(len(s)):
            if s[i:i+k] == v:
                p.append(i)

        # assuming len(p) &gt; 1
        factor = p[1] - p[0]
        for i in range(2, len(p)):
            factor = math.gcd(factor, p[i] - p[i - 1])

        locations = &quot;&quot;
        for i in range(len(p)):
            locations += f&quot;{p[i]} &quot;
            if i &gt; 0:
                locations += f&quot;({p[i] - p[i-1]}) &quot;

        out += f&quot;{k:6d}  {found[k][v]:5d}  {v:10s}  {factor:6d}  {locations}\n&quot;

    return out

i, k = 1, 0
min_num = 3
infile, outfile = None, None
while i &lt; len(argv):
    if argv[i] == &apos;-v&apos;:
        is_debug = True
        debug(f&quot;debug: {is_debug}&quot;)
    elif argv[i] == &apos;-m&apos;:
        i += 1
        min_num = int(argv[i])
        debug(f&quot;min_num: {min_num}&quot;)
    elif argv[i][0] != &apos;-&apos;:
        if k == 0:
            infile = argv[i]
            debug(f&quot;infile: {infile}&quot;)
        elif k == 1:
            outfile = argv[i]
            debug(f&quot;outfile: {outfile}&quot;)
        k += 1
    i += 1

s = None
if infile is None:
    s = input()
else:
    with open(infile, &apos;r&apos;) as f:
        s = f.read()

out = kasiski(s, min_num)
if outfile is None:
    print(out)
else:
    with open(outfile, &apos;w&apos;) as f:
        f.write(out)
```

---

## 参考文献

- [Kasiski Examination - Wikipedia](https://en.wikipedia.org/wiki/Kasiski_examination)

</content:encoded></item><item><title>密码学学习笔记 - 凯撒密码</title><link>https://www.jason-z.com/posts/crypto-learning-notes-caesar-cipher/</link><guid isPermaLink="true">https://www.jason-z.com/posts/crypto-learning-notes-caesar-cipher/</guid><pubDate>Thu, 06 Oct 2022 06:26:00 GMT</pubDate><content:encoded>

凯撒密码是一种古典加密算法。

## 加密原理

凯撒密码的加密算法的核心原理实际上就是替换加密。

将明文中的字母，通过偏移一定量，替换为新的字母生成密文。

假设偏移量为+3（即向后偏移），那么可以生成对应的明文和密文字母表：

明文字母表：ABCDEFGHIJKLMNOPQRSTUVWXYZ  
密文字母表：DEFGHIJKLMNOPQRSTUVWXYZABC

![Caesar cipher -
Wikipedia](https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Caesar_cipher_left_shift_of_3.svg/1200px-Caesar_cipher_left_shift_of_3.svg.png)

实际使用中，直接替换，就可以进行加密和解密了，例如：

**明文**：THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG  
**密文**：WKH TXLFN EURZQ IRA MXPSV RYHU WKH ODCB GRJ

凯撒密码可以利用数学中的同余算法来进行计算，将字母用数字代替，A=0，B=1，...，Z=25。

其中 x 为明文/密文字母，n 为偏移量。

### 同余算法

两个整数共同除以一个整数，如果他们的余数相同，则两个整数称为同余。

## Python实现

### 加密代码

```python
# A python program to illustrate Caesar Cipher Technique
def encrypt(text, s):
    result = &quot;&quot;
    # traverse text
    for i in range(len(text)):
        char = text[i]

        # Encrypt uppercase characters
        if char.isupper():
            result += chr((ord(char) + s - 65) % 26 + 65)

        # Encrypt lowercase characters
        else:
            result += chr((ord(char) + s - 97) % 26 + 97)

    return result

# Check the above function
text = &quot;ATTACKATONCE&quot;
s = 4
print(&quot;Text  : &quot; + text)
print(&quot;Shift : &quot; + str(s))
print(&quot;Cipher: &quot; + encrypt(text, s))
```

## 破解方法

在已知偏移量的情况下的破解方法在上面的破解原理里已经说的较为清楚了，我们直接上代码

```python
#A python program to illustrate Caesar Cipher Technique
def encrypt(text,s):
    result = &quot;&quot;
 
    # traverse text
    for i in range(len(text)):
        char = text[i]
 
        # Encrypt uppercase characters
        if (char.isupper()):
            result += chr((ord(char) + s-65) % 26 + 65)
 
        # Encrypt lowercase characters
        else:
            result += chr((ord(char) + s - 97) % 26 + 97)
 
    return result
 
#check the above function
text = &quot;ATTACKATONCE&quot;
s = 4
print (&quot;Text  : &quot; + text)
print (&quot;Shift : &quot; + str(s))
print (&quot;Cipher: &quot; + encrypt(text,s))
```

1）穷举法，实际我们的最大偏移量最多也就25，那么我们只要将密文分别进行1-25的偏移量解密，然后通过观察和分析解密后的明文的语义，基本上就能找出对应的偏移量。

2）频率分析法：正常情况下，对我们对明文中进行频率分析，会得出出现最高的单词应该为E，那么，我们同样对密文也进行频率分析，得出频率最高的单词，那么就可以计算出偏移量。

图例中为偏移量为1时，加密前后的字母频率分析对比。可以很清晰地看出之前为E，之后为F，从而推断偏移量基本为1.

![](https://miro.medium.com/max/1400/1*nZnP-Rpr-psKqSGoIXomvA.png)

当然，这种破解方法和密文的长度还是有关系的，密文长度越长，更接近自然语言，准确率越高。



## 参考文献

&lt;https://en.wikipedia.org/wiki/Caesar_cipher&gt;</content:encoded></item></channel></rss>