开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

鸿蒙Next开发之-解决鸿蒙next开发如何给测试同学测试包

发表于 2024-04-26

前言

目前公司和华为达成了合作,计划在鸿蒙正式发布next版本之前,上架公司APP的鸿蒙版本。方便用户升级手机到鸿蒙next之后,能正常使用公司APP,保证用户的既得利益。

最近我们计划在4月底上线公司的鸿蒙第一版APP,在开发和测试中遇到一个问题。那就是如何把开发同学的APP包直接丢给测试同学进行测试。由于这一块鸿蒙可能借鉴了iOS的经验,导致我们目前没办法直接给到测试同学一个APP,然后进行安装。因此目前我们有如下做法:

  • 1、每次改动都让测试拿手机来开发同学电脑上进行打包安装测试。
  • 2、测试直接安装鸿蒙开发环境,将代码拉到本地,运行指定分支的代码进行安装测试。

以上方法可想而知,对开发和测试都是极其不友好的。

因此考虑是否能进行优化这其中的流程。

开发环境

  • Mac
  • DevEco Studio NEXT Developer Preview2
  • HarmonyOS next Developer Preview2
  • java version “11.0.18” 2023-01-17 LTS
  • hdc 1.2.0a
  • 手机:Mate 60 (HarmonyOS NEXT Developer Preview2)

项目代码架构

  • 方式1:单HAP+可选(多HSP)+可选(多HAR)
  • 方式2:多HAP+可选(多HSP)+可选(多HAR)

目前我们项目采用的是方式1

思考

目前鸿蒙next版本的APP想要安装到鸿蒙测试手机上,(都必须得对APP进行签名)。

HarmonyOS通过数字证书(.cer文件)和Profile文件(.p7b文件)来对应用/元服务进行管控。

因此只有签名过的HAP才能安装到设备上运行。
有以下两种方式:

  • 1、将包上传到华为的AppGallery Connect,AppGallery Connect会重新解压APP,然后签名,之后你就可以在华为的应用商店进行下载安装。
  • 开发者直接使用手机连接idea,使用自动签名or手动签名进行安装

就目前来看,好像没有办法像Android一样丢一个APK给测试,或者给他一个网站,让测试下载下来直接安装。
真的就不行吗?确实不行,但可以优化。

DevEco Studio如何安装APP

然后我观察到idea是如何将HAP安装到测试手机上。在控制台我看到如下输出:

1
2
3
4
5
6
7
8
shell复制代码$ hdc shell aa force-stop com.byhk.app
$ hdc uninstall com.byhk.app in 669 ms
$ hdc shell mkdir data/local/tmp/e865b2e1cb8147408400c284260da55c
$ hdc file send /XXX/xxx/build/default/outputs/default/xxx-signed.hsp "data/local/tmp/e865b2e1cb8147408400c284260da55c" in 22 s 117 m
$ hdc file send /XXX/app/build/default/outputs/default/app-default-signed.hap "data/local/tmp/e865b2e1cb8147408400c284260da55c" in 1 min 50 s 580 ms
$ hdc shell bm install -p data/local/tmp/e865b2e1cb8147408400c284260da55c in 11 s 907 ms
$ hdc shell rm -rf data/local/tmp/e865b2e1cb8147408400c284260da55c
$ hdc shell aa start -a AppAbility -b com.byhk.app -D in 89 ms

简单介绍一下上面的命令作用:

  • hdc shell aa force-stop:强制停止APP,后面跟包名
  • hdc uninstall:卸载APP,后面跟包名
  • hdc shell mkdir:使用shell命令创建一个目录。
  • hdc file send:将hap和hsp发送到这个目录中,可能有人会问,为什么没有HAR。因为HAR的代码复制到了其依赖的module里面了。
  • hdc shell bm install -p:安装这个目录里面所有的东西
  • hdc shell rm -rf:移除刚刚的目录
  • hdc shell aa start -a AppAbility -b 包名 -D:启动APP

需要注意的是,send到这个目录的HAP,和HSP,都是进行了签名的。

重点:这个时候我就在想,如果我有一个已经签名的.app文件,我直接给到测试或者丢到一个网站上。并且给他写一份脚本,这个脚本将APP进行解压,然后把解压后的hap、hsp文件send到这个目录进行安装,是不是就行了。有道理。说干就干。

生成.app

这个很简单直接在DevEco Studio上进行操作。操作如下:

image.png

最后会在build目录下生成两个.app文件,如下:

image.png

其中一个签名了,一个未签名。

这儿有个坑,app签名了,但你解压之后的hap、hsp是没有进行签名的。

因此我需要对HAP和HSP分别进行签名。

对HAP、HSP进行签名

对HAP和HSP进行签名,需要java环境和鸿蒙的签名工具:hap_sign_tool.jar

hap_sign_tool.jar目录: 在${HOS_SDK_HOME}\HarmonyOS-NEXT-DP2\base\toolchains\lib,
如图:

image.png

使用如下命令对HAP进行签名。

1
matlab复制代码java -jar hap-sign-tool.jar sign-app -keyAlias "key0" -signAlg "SHA256withECDSA" -mode "localSign" -appCertFile "test.cer" -profileFile "test.p7b" -inFile "hap-unsigned.hap" -keystoreFile "test.p12" -outFile "result\hap-signed.hap" -keyPwd "123456" -keystorePwd "123456" -signCode "1"

关于该命令中需要修改的参数说明如下,其余参数不需要修改:

  • keyAlias:密钥别名。
  • appCertFile:申请的调试证书文件,格式为.cer。
  • profileFile:申请的调试Profile文件,格式为.p7b。
  • inFile:通过hvigor打包生成的未携带签名信息的HAP。
  • keystoreFile:密钥库文件,格式为.p12。
  • outFile:经过签名后生成的携带签名信息的HAP。
  • keyPwd:密钥口令。
  • keystorePwd:密钥库口令。
  • signCode:是否开启代码签名,缺省为开启。1表示开启,0表示不开启。从NEXT Developer Preview2版本开始,需开启代码签名,否则将导致构建出的包无法安装到设备上。

脚本编写

这儿我使用的是Python语言,脚本分为:

  • 签名脚本
  • 安装脚本

签名脚本

1、解压.app文件

1
2
3
4
5
6
7
python复制代码def unzip_app(app_file, signed_dir, output_dir):
print("开始解压APP")
os.makedirs(output_dir, exist_ok=True)
os.makedirs(signed_dir, exist_ok=True)
with zipfile.ZipFile(app_file, 'r') as zip_ref:
zip_ref.extractall(output_dir)
print("解压成功")

2、签名Hap、Hsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码def signed_app(extracted_dir, signed_dir):
print("开始签名APP")
sign_files(extracted_dir, signed_dir, '.hap')
sign_files(extracted_dir, signed_dir, '.hsp')
print("签名完成")

def sign_files(source_dir, target_dir, extension):
files = [f for f in os.listdir(source_dir) if f.endswith(extension)]
for file in files:
sign_file(source_dir, target_dir, file)

def sign_file(source_dir, target_dir, file):
sign_command = f"java -jar {hap_sign_tool} sign-app -keyAlias {alias} -signAlg \"SHA256withECDSA\" -mode \"localSign\" -appCertFile \"{cer_path}\" -profileFile \"{profile_file_path}\" -inFile \"{os.path.join(source_dir, file)}\" -keystoreFile \"{k12_path}\" -outFile \"{target_dir}/{file.split('.')[0]}-signed.{file.split('.')[1]}\" -keyPwd \"{pwd}\" -keystorePwd \"{pwd}\""
subprocess.run(sign_command, shell=True)

3、重新压缩生成zip包

1
2
3
4
5
6
7
8
lua复制代码def zip_signed_files(signed_dir):
os.makedirs(os.path.dirname(out_put_file_path), exist_ok=True)
print("开始压缩已签名文件")
with zipfile.ZipFile(out_put_file_path, 'w') as zip_ref:
for root, _, files in os.walk(signed_dir):
for file in files:
zip_ref.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), signed_dir))
print("压缩完成")

4、其他常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码app_file = "xxx/jojo-read-default-signed.app" //app地址
hap_sign_tool = "xxx/Huawei/Sdk/HarmonyOS-NEXT-DP2/base/toolchains/lib/hap-sign-tool.jar" //签名工具地址
cer_path = "xxx/app.cer" //cer地址
profile_file_path="xxx/app.p7b" //prpfile地址
k12_path = "xxx/app.p12" //.p12地址
alias = "ohos"
pwd = "123456" //密码

app_file_path = os.path.dirname(app_file)
file_name = os.path.basename(app_file)
package_name = "com.byhk.app" //包名
extracted_dir = app_file_path + "/zip"
signed_dir = app_file_path + "/signed"
out_put_file_dir = app_file_path + "/out/"
out_put_file_path = out_put_file_dir + file_name

完成以上,就会在out目录下生成一个.zip文件,这个文件就可以丢给测试了。但测试还需要将其解压send到手机上,因此还需要一个安装脚本。

安装脚本

1、解压文件

1
2
3
4
5
6
python复制代码def unzip_app(app_file, output_dir):
print("开始解压APP")
os.makedirs(output_dir, exist_ok=True)
with zipfile.ZipFile(app_file, 'r') as zip_ref:
zip_ref.extractall(output_dir)
print("解压成功")

2、停止并卸载应用

1
2
3
4
5
6
python复制代码def stop_and_uninstall_app(package_name):
print("停止并卸载应用")
subprocess.run(f"hdc shell aa force-stop {package_name}", shell=True)
subprocess.run(f"hdc uninstall {package_name}", shell=True)
subprocess.run("hdc shell rm -rf data/local/tmp/6927085eaa4645068468e40d6c797292", shell=True)
subprocess.run("hdc shell mkdir data/local/tmp/6927085eaa4645068468e40d6c797292", shell=True)

3、发送解压文件到手机

1
2
3
4
5
scss复制代码def send_files_to_device(extracted_dir, package_name):
print("发送文件到设备")
files = [f for f in os.listdir(extracted_dir) if f.endswith('.hap') or f.endswith('.hsp')]
for file in files:
subprocess.run(f"hdc file send {os.path.join(extracted_dir, file)} data/local/tmp/6927085eaa4645068468e40d6c797292", shell=True)

4、安装app并启动应用

1
2
3
4
5
6
python复制代码def start_app(package_name):
print("启动应用")
subprocess.run(f"hdc shell bm install -p data/local/tmp/6927085eaa4645068468e40d6c797292", shell=True)
subprocess.run(f"hdc shell rm -rf data/local/tmp/6927085eaa4645068468e40d6c797292", shell=True)
subprocess.run(f"hdc shell aa start -a AppAbility -b {package_name}", shell=True)
print("启动成功")

5、其他常量

1
2
3
4
5
6
ini复制代码app_file = "xxx/app-signed.zip"
package_name = "com.byhk.app"

app_file_path = os.path.dirname(app_file)
extracted_dir = app_file_path + "/zip"
unzip_app_and_send_files(app_file, package_name, extracted_dir)

只需要测试在本地安装hdc命令环境和python环境,配置好zip包路径,即可运行脚本安装app。

后续计划使用flutter写一个桌面工具,包含app下载和安装。

完整脚本,放在文章末尾。有需要的自提。

以下是关于自动签名和手动签名的方法。

自动签名

这种方式,仅限于开发者自己调试使用。方便快捷。操作如下:

进入File > Project Structure… > Project > Signing Configs界面,勾选“Automatically generate signature”(如果是HarmonyOS工程,需同时勾选“Support HarmonyOS”),即可完成签名。如果未登录,请先单击Sign In进行登录,然后自动完成签名。如下:

image.png

手动签名

相比自动签名,手动签名就相对来说比较麻烦一点。官方文档:developer.huawei.com/consumer/cn…

操作流程图

image.png

准备证书请求文件

需要通过DevEco Studio来生成密钥(.p12)和证书请求文件(.csr)。

生成密钥(.p12)

  1. 在顶部菜单栏选择“Build > Generate Key and CSR”。

image.png

  1. 点击“New”

image.png

  • Key store file:设置密钥库文件存储路径,并填写p12文件名。
  • Password:设置密钥库密码,必须由大写字母、小写字母、数字和特殊符号中的两种以上字符的组合,长度至少为8位。请记住该密码,后续签名配置需要使用。
  • Confirm password:再次输入密钥库密码。

生成证书请求文件(.csr)

image.png

  • Alias:密钥的别名信息,用于标识密钥名称。请记住该别名,后续签名配置需要使用。
  • Password:密钥对应的密码,与密钥库密码保持一致,无需手动输入。
  • Validity:证书有效期,建议设置为25年及以上,覆盖应用/元服务的完整生命周期。
  • Certificate:输入证书基本信息,如组织、城市或地区、国家码等。

点击“Next”

20240426145120.jpg

选择路径和输入名称及其后缀。

至此完成了。密钥(.p12)和证书文件(.csr),Android同学很熟悉。

申请调试证书

登录AppGallery Connect,选择“用户与访问”。

image.png
在左侧导航栏选择“证书管理”,进入证书管理页面,。

image.png

点击右上角“新增证书”

image.png

选择刚刚生成的csr文件。

image.png

会生成一个cer格式的证书文件,将其报错下来。

注册调试设备

在刚刚的证书管理下面,点击设备管理。

image.png
如果有多个设备,可以批量添加

申请调试的profile

1、登录AppGallery Connect,选择“我的项目”。
2. 找到您的项目,点击您创建的HarmonyOS应用/元服务。

image.png

  1. 选择“HarmonyOS应用 > HAP Provision Profile管理”,进入“管理HAP Provision Profile”页面,点击右上角“添加”。

image.png

至此,我们已经完成了profile的文件生成。(后缀名为.p7b),将其下载下来保存到本地。

(备注:每次新增设备,都需要重新生成调试profile文件)

目前我们电脑上有以下文件了:

image.png

脚本

  • 签名脚本
  • 安装脚本

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Volo 0100 正式发布!

发表于 2024-04-26

图片

Volo 是字节跳动服务框架团队研发的 高性能、可扩展性强 的 Rust RPC 框架,使用了 Rust 最新的 AFIT 和 RPITIT 特性。

文章来源|CloudWeGo 开源社区

Volo 0.10.0 版本中,我们更多地关注可扩展性和易用性。

01 Break Change

错误处理重构

原先的错误类型存在诸如语义不清晰、可维护性不强、可扩展性不强、容易误用等问题,因此在新版中,我们重构了整个错误处理部分,极大加强了错误处理部分的语义清晰度和可维护性,并通过类型系统降低误用概率。

迁移指南

Server Handler 迁移
  1. 如果原先使用 anyhow::Result,把anyhow:Result改为 volo_thrift::ServerResult 即可:

图片

  1. 如果原先使用 Result<XResp, anyhow::Error> ,将 anyhow::Error 改为 volo_thrift::ServerError 即可:

图片

  1. 如果原先使用了 Exception 的用户,需要将返回类型 Result<XResp, volo_thrift::UserException<XException>>改为Result<volo_thrift::MaybeException<XResp, XException>,volo_thrift::ServerError>,同时将原先返回 Err(UserError::UserException(exception)) 的地方改为使用 Ok(MaybeException::Exception(exception)) 即可:

图片

  1. 如果改完之后,在返回 anyhow::Error 时出现报错,可以手动加一个 .into()。

图片

  1. 如果改完之后,在 ? 返回错误处出现报错,可以尝试先转换成 anyhow::Error 再返回。

图片

Service 中间件迁移

对于不感知用户错误的中间件来说,本次修改应该不带来 break change;如果有需要感知用户错误,那么只需要把原来的 volo_thrift::Error 改为 volo_thrift::ServerError/ClientError 即可。

Client 迁移

client 部分的错误从原来的 ResponseError 改为了 ClientError,按编译器报错提示匹配新的错误 variant 即可。

IDL 管理文件 volo.yml 格式重构

新版 yml 配置的结构更加清晰,且更易于维护,并主要解决了旧版中无法支持 git 跨仓库引用的问题,具体的功能和配置参数见 config。另外,对于 volo-cli 命令行工具,我们将之前的 idl 命令名字修改为了 repo。

config: www.cloudwego.io/zh/docs/vol…

迁移指南

安装 volo-cli 0.10.0 版本,并在 volo.yml 目录下执行 volo migrate 命令即可自动迁移。

默认生成的 Enum 类型修改

在新版生成代码中,默认生成的 Enum 类型修改为了 i32 wrapper 的 newtype 类型,以便于更好的向前兼容 IDL enum 字段中枚举值的修改。

迁移指南

将 enum 字段中枚举值名字修改为对应生成的名字即可,如 Foo::Bar -> Foo::BAR。

02 完整 Release Note

完整的 Release Note 可以参考 Volo Changelog。

项目地址

GitHub:github.com/cloudwego

官网:<www.cloudwego.io>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Sublime Text 3配置C/C++开发环境 一、引言

发表于 2024-04-26

《开发工具系列》

image.png

一、引言

C/C++ 的开发工具有很多,相信大家多多少少都有所了解,比如 Visual C++、Codeblocks、VSCode、Dev C++ 等等;本篇 Huazie 介绍一个比较轻量级的开发环境 Sublime Text 3,并用它来配置 C/C++ 开发环境。

二、主要内容

2.1 初识 Sublime Text 3

Sublime Text 3 是一款流行的文本编辑器,它的特点是体积小巧、启动速度快、界面简洁美观。它具有强大的代码编辑功能,支持多种编程语言。此外,Sublime Text 3 还具有丰富的插件生态系统,用户可以根据自己的需求安装各种插件来扩展其功能。

Sublime Text 3 的一些主要特点,如下所示:

  • 强大的代码编辑功能:Sublime Text 3 提供了许多实用的代码编辑功能,如自动完成、代码高亮、代码片段等,大大提高了编程效率。
  • 支持多种编程语言:Sublime Text 3 支持多种编程语言,包括 HTML、CSS、JavaScript、Python、Ruby、PHP 等,用户可以根据需要选择不同的语言模式。
  • 插件生态系统:Sublime Text 3 拥有丰富的插件生态系统,用户可以通过安装插件来扩展其功能,如 Emmet(用于编写 HTML 和 CSS)、Package Control(用于安装和管理插件)等。
  • 自定义快捷键:Sublime Text 3 允许用户自定义快捷键,以便更快速地执行常用操作。
  • 多窗口编辑:Sublime Text 3 支持多窗口编辑,用户可以同时打开多个文件进行编辑,方便进行代码对比和复制粘贴操作。
  • 跨平台支持:Sublime Text 3 支持 Windows、Mac 和 Linux 操作系统,用户可以在不同的平台上使用相同的设置和插件。
  • 版本控制集成:Sublime Text 3 可以与版本控制系统(如 Git)集成,方便用户进行代码版本管理。

2.2 接入 mingw-w64

Mingw-w64 是一个用于在 Windows 系统上支持 GCC 编译器的完整运行时环境。这个项目是原始 Mingw.org 项目的进一步发展,旨在支持 64位 和 32位 Windows 操作系统原生的二进制文件。在 2007 年,Mingw-w64 从原始 Mingw.org项目分叉出来,提供对 64位和 新API 的支持。从那时起,它逐渐得到了广泛的应用和传播。

Mingw-w64 提供了一百万行以上的头文件、库和运行时,用于在 Windows 上链接和运行代码。它包括 Winpthreads 库(用于 C++11 线程支持)和 Winstorecompat库(一个便利库,可简化与 Windows 应用商店的一致性)。与VisualStudio 相比,Mingw-w64 在数学支持方面更为完善,并且执行速度更快。

2.2.1 下载 mingw-w64

参考这个帖子内容,下载一下 mingw-w64:

tieba.baidu.com/p/548754485…

我目前用的 MinGW-w64 9.0.0,有需要的可以在评论回复 或者 私信我。

2.2.2 环境变量配置

不管是安装版的,还是免安装版的,都会有类似如下的目录:

image.png

上图中的 bin 目录,我们点进去看下:

image.png

实际上这里的 gcc.exe 和 g++.exe 就是 C/C++ 程序编译和运行的关键所在,当然这里还有其他的可执行程序,感兴趣的朋友可以自行搜索了解,这里不展开了。

现在,Huazie 以 window 11 系统为例,介绍下配置环境变量,如下:

右击 Window 图标,打开下图并选择 系统:

image.png

点击 高级系统设置,打开系统属性页面,点击 环境变量 :

image.png

找到 Path 环境变量,配置上面你的 mingw64 的 bin 目录进去:

image.png

2.3 配置 C/C++ 开发环境

打开 Sublime Text 3,我们可以看到如下的界面:

image.png

菜单栏选择 Tools => Build System => New Build System

image.png

2.3.1 C Build System 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
c复制代码{
"windows": {
"cmd": ["gcc", "-std=c11", "${file}", "-o", "${file_base_name}.exe"],
},
"cmd": ["gcc", "-std=c11", "${file}", "-o", "${file_base_name}"],
"file_regex": "^(.*)\\(([0-9]+),([0-9]+)\\) (Error|Fatal): (.*)$",
"working_dir": "${file_path}",
"selector": "source.c",
"encoding": "cp936",
"variants": [
{
"name": "Run",
"shell": true,
"windows": {
"shell_cmd" : "start cmd /c \"\"${file_base_name}.exe\" & echo. & pause\""
}
},
{
"name": "Build and Run",
"shell": true,
"windows": {
"shell_cmd": "gcc -std=c11 \"${file}\" -o \"${file_base_name}.exe\" && start cmd /c \"\"${file_base_name}.exe\" & echo. & pause\""
},
}
]
}

上述内容保存在,前面打开的 New Build System 中,并命名为 C.sublime-build:

image.png

2.3.2 C++ Build System 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码{  
"cmd": ["g++", "${file}", "-o", "${file_path}/${file_base_name}", "-Wall" ,"&&","start","cb_console_runner.exe","${file_path}/${file_base_name}"],
"file_regex": "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$",
"working_dir": "${file_path}",
"selector": "source.c, source.c++",
"shell": true,
"encoding": "cp936",
"variants":
[
{
"name": "Run",
"cmd": ["start","cb_console_runner.exe","${file_path}/${file_base_name}"]
}
]
}

重新 New Build System,将上述内容保存其中,并命名为 CPP.sublime-build

image.png

2.4 编写 C/C++ 代码

现在让我们开始编写吧!!!

2.4.1 第一个 C 代码【helloworld.c】

1
2
3
4
5
6
7
8
c复制代码#include<stdio.h>

int main()
{
printf("hello world!\n");
printf("[C]Author: Huazie");
return 0;
}

2.4.2 第一个 C++ 代码【helloworld.cpp】

1
2
3
4
5
6
7
8
9
cpp复制代码#include <iostream>
using namespace std;

int main()
{
cout << "Hello World" << endl;
cout << "[C++]Author: Huazie";
return 0;
}

2.5 运行 C/C++ 代码

2.5.1 运行 C 代码

菜单栏 Tools => Build System ,然后 选择 C,就是前面的 C.sublime-build。

然后直接 Ctrl + B,编译运行当前的程序,运行截图如下所示:

image.png

2.5.2 运行 C++ 代码

菜单栏 Tools => Build System ,然后 选择 CPP,就是前面的 CPP.sublime-build。

然后直接 Ctrl + B,编译运行当前的程序,运行截图如下所示:

image.png

三、总结

本篇 Huazie 介绍了 Sublime Text 3 配置 C/C++ 的相关内容,感兴趣的朋友赶紧配置起来,有任何问题可以随时评论区沟通。

四、更新

经过上面的配置,大家都能写自己的第一个 C/C++ 代码了,但是有些小伙伴慢慢使用发现,如果输出的内容包含中文,打印出来的信息是乱码的。有关这个问题,请查看笔者的另一篇博文《Sublime Text 3 解决中文乱码问题》。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

未命名

发表于 2024-04-26

这里每天分享一个 iOS 的新知识,快来关注我吧

苹果官宣 5 月 7 日举办“Let Loose”活动

Apple 宣布将于北京时间 5 月 7 日晚上 10 点举行一场特别活动,预计将发布新的 iPad Pro 和 iPad Air 型号,以及更新的 Apple Pencil 和妙控键盘等配件。

以下是一些关于新 iPad 的一些预测:

  1. 配备 M3 芯片
  2. OLED 显示屏
  3. 更薄的外壳、更薄的边框
  4. 支持横向前置摄像头
  5. 支持 iPad Pro 的全新妙控键盘
  6. 新的 Apple Pencil,并可能支持 Vision Pro

在苹果的历史上,5 月只举办过两场苹果活动:1999 年的 WWDC,和 1998 年 5 月的第一代 iMac 发布!这次是第三场。

iOS 17.5 Beta 3 发布

Apple 发布了即将推出的 iOS 17.5 和 iPadOS 17.5 更新的第三个测试版。

本次更新更改包括 Apple News+、 Game Center 排行榜集成、播客小组件的颜色匹配等功能。

苹果削减 Vision Pro 出货量

根据郭明錤的一份分析报告,苹果已经下调了 Vision Pro 的出货量预期。苹果现在的目标是今年 Vision Pro 的出货量在 40 万至 45 万台之间。这低于最初市场共识的 70 万至 80万台。

Vision Pro 在美国的需求“大幅下降,超出了预期”,这直接导致了其出货量削减。

另外该分析透露,苹果将在 6 月的 WWDC 之前在其他国家/地区发布 Vision Pro。

iPhone 16 将配备电容触摸按钮

据经济日报报道,iPhone 16 可能会配备新的触摸电容式按钮。

这些按钮会提供触觉反馈,而不是传统的物理按钮,有点类似于 iPhone 7 上的 Home 按钮。

苹果收购法国人工智能公司

苹果收购了总部位于巴黎的人工智能初创公司 Datakalab,该公司专注于算法压缩和嵌入式 AI 系统。

这项收购于去年 12 月 17 日完成,是悄悄进行的。虽然该交易的财务细节尚未披露,但此举可以肯定是苹果 AI 战略的一部分,预计将在 iOS 18 中引入的 iPhone 16 等设备。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用python 下载&替换markdown文章中的图床url

发表于 2024-04-26

1、问题&困扰

下边这个问题困扰我有段时间了:

因为文章是在掘金,知乎这类技术网站发表的,在发表时文章中的图片都是上传到了掘金 or 知乎的图床上了,不管是用的哪家哪个大厂的图床,我总是担心哪一天网站崩掉或者其他什么不可控的事情发生,那我博客中的所有的图床文件(大部分都是图片)那岂不是都访问不到了? 像这样:
image.png
这就尴尬了,我一般写文章很重视图解,没有图片的文章那没法串联起来,所以我总是想的把文章复制到本机上,但是复制到本地上的文章中的图片不用说也仍然是访问的三方的图床呀,如下:
image.png

2、我的目标

  1. 将这类三方图床文件下载到本地
  2. 将markdown中的图传连接替换为指向本机的图片路径,如下:image.png

我不可能一个个手动去下载替换,那样士可忍孰不可忍。所以我决定使用python批量替换,基本逻辑是:

  1. 找到替换前的markdown文章集合
  2. 依次读取文章内容,并使用正则找到图床文件,并下载到本地(会在当前目录创建个专门放图片的文件夹)
  3. 替换markdown中的图床链接为本地的图片路径。
  4. 另外我还生成了word文件,以作备份。

3、使用python实现

读取指定文件夹中的文章集合,并解析,正则匹配与下载替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
python复制代码
from functools import partial

from util.download_util import download_pic
from util.file_util import is_dir_existed, search_all_file, read_file_text_content, write_text_to_file
from util.logger_util import default_logger
import os
import re
import time

logger = default_logger()
origin_md_dir = os.path.join("/Users/hzz/Documents/黄壮壮_写作/已发表的博客_掘金存储图片/") # 原始md文件目录
local_md_dir = os.path.join("/Users/hzz/Documents/黄壮壮_写作/已发表博客_本地存储图片/") # 本地md文件目录
local_doc_dir = os.path.join("/Users/hzz/Documents/黄壮壮_写作/已发表博客_doc/") # 本地doc文件目录
server_md_dir = os.path.join(os.getcwd(), "server_md") # 转换成自己的图片服务器md文件目录
pic_match_pattern = re.compile(r'(]: |()+(http.*?.(png|PNG|jpg|JPG|gif|GIF|svg|SVG|webp|awebp|image))??()?)',
re.M) # 匹配图片的正则
order_set = {i for i in range(1, 500000)} # 避免图片名重复后缀
generate_server_md = False # 是否在本地化后生成为自己图床的md文件
generate_doc = True # 是否在本地化后生成doc文件


# 检索md文件
def retrieve_md(file_dir):
logger.info("检索路径 → %s" % file_dir)
md_file_list = search_all_file(file_dir, target_suffix_tuple=('md', "MD"))
file_name = input('请输入指定的文件: ')

if len(file_name)!=0:
filtered_files = []
for filename in md_file_list:
if file_name in filename:
# 这里可以添加更多逻辑处理
filtered_files.append(filename)
md_file_list=filtered_files

if len(md_file_list) == 0:
logger.info("未检测到Markdown文件,请检查后重试!")
exit(-1)
else:
logger.info("检测到Markdown文件 → %d个" % len(md_file_list))
logger.info("=" * 64)
for pos, md_file in enumerate(md_file_list):
logger.info("%d、%s" % (pos + 1, md_file))
logger.info("=" * 64)
logger.info("执行批处理操作")
process_md(md_file_list)


# 处理文件列表
def process_md(file_list):
for file in file_list:
to_local_md(file)


# 转换成本地MD
def to_local_md(md_file):
logger.info("处理文件:【%s】" % md_file)
# 获取md文件名
md_file_name = os.path.basename(md_file)
# 生成md文件的目录、图片目录,doc目录
new_md_dir = os.path.join(local_md_dir, md_file_name[:-3])
new_picture_dir = os.path.join(new_md_dir, "images")
is_dir_existed(new_md_dir)
is_dir_existed(new_picture_dir)
# 生成md文件路径
new_md_file_path = os.path.join(new_md_dir, md_file_name)
new_doc_file_path = os.path.join(local_doc_dir, md_file_name[:-3].replace("知乎盐选 ", "") + ".docx")
# 读取md文件内容
old_content = read_file_text_content(md_file)
# 替换原内容
new_content = pic_match_pattern.sub(partial(pic_to_local, pic_save_dir=new_picture_dir), old_content)
# 生成新的md文件
write_text_to_file(new_content, new_md_file_path)
logger.info("新md文件已生成 → {}".format(new_md_file_path))
# 生成新的doc文件
if generate_doc:
os.chdir(new_md_dir)
logger.info("新doc文件已生成 → {}".format(new_doc_file_path))
logger.info("=" * 64)
if generate_server_md:
to_server_md(new_md_file_path)


# 远程图片转换为本地图片
def pic_to_local(match_result, pic_save_dir):
logger.info("替换前的图片路径:{}".format(match_result[2]))
# 生成新的图片名
img_file_name = "{}_{}.{}".format(int(round(time.time())), order_set.pop(), "png")# match_result[3]
# 拼接图片相对路径(Markdown用到的)
relative_path = 'images/{}'.format(img_file_name)
# 拼接图片绝对路径,下载到本地
absolute_path = os.path.join(pic_save_dir, img_file_name)
logger.info("替换后的图片路径:{}".format(relative_path))
# 下载图片
download_pic(absolute_path, match_result[2])
# 还需要拼接前后括号()
return "{}{}{}".format(match_result[1], relative_path, match_result[4])

# 转换成自己的图片服务器md文件
def to_server_md(md_file):
pass


if __name__ == '__main__':
is_dir_existed(origin_md_dir)
is_dir_existed(local_md_dir, is_recreate=True)
is_dir_existed(local_doc_dir, is_recreate=True)
is_dir_existed(server_md_dir, is_recreate=True)
# loop = asyncio.get_event_loop()
retrieve_md(origin_md_dir)

下载图片 download_util 工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
python复制代码

import os
import time

import requests


from util.logger_util import default_logger

logger = default_logger()

default_headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept-Encoding': 'gzip, deflate, br',
'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"macOS"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?0',
'Upgrade-Insecure-Requests': '1'
}


def download_pic(pic_path, url, headers=None):
try:
if headers is None:
headers = default_headers
if url.startswith("http") | url.startswith("https"):
if os.path.exists(pic_path):
logger.info("图片已存在,跳过下载:%s" % pic_path)
else:
res1 = requests.get(url, headers=headers,verify=False)

# res1.encoding = "utf8"
with open(pic_path, "wb+") as f:
f.write(res1.content)
else:
logger.info("图片链接格式不正确:%s - %s" % (pic_path, url))
time.sleep(1)
except Exception as e:
logger.info("下载异常:{}\n{}".format(url, e))

logger 工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码
import logging


# 默认日志工具
def default_logger():
custom_logger = logging.getLogger("CpPythonBox")
if not custom_logger.hasHandlers():
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
fmt='%(asctime)s %(process)d:%(processName)s- %(levelname)s === %(message)s',
datefmt="%Y-%m-%d %H:%M:%S %p"))
custom_logger.addHandler(handler)
custom_logger.setLevel(logging.INFO)
return custom_logger

file_util 工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
python复制代码
def search_all_file(file_dir=os.getcwd(), target_suffix_tuple=()):
""" 递归遍历文夹与子文件夹中的特定后缀文件

Args:
file_dir (str): 文件目录
target_suffix_tuple (Tuple(Str)): 文件目录

Returns:
list : 文件路径列表
"""
file_list = []
# 切换到目录下
os.chdir(file_dir)
file_name_list = os.listdir(os.curdir)
for file_name in file_name_list:
# 获取文件绝对路径
file_path = "{}{}{}".format(os.getcwd(), os.path.sep, file_name)
# 判断是否为目录,是往下递归
if os.path.isdir(file_path):
# print("[-]", file_path)
file_list.extend(search_all_file(file_path, target_suffix_tuple))
os.chdir(os.pardir)
elif target_suffix_tuple is not None and file_name.endswith(target_suffix_tuple):
# print("[!]", file_path)
file_list.append(file_path)
else:
pass
# print("[+]", file_path)
return file_list



def write_text_to_file(content, file_path, mode="w+"):
""" 将文字写入到文件中

Args:
content (str): 文字内容
file_path (str): 写入文件路径
mode (str): 文件写入模式,w写入、a追加、+可读写

Returns:
None
"""
with lock:
try:
with open(file_path, mode, encoding='utf-8') as f:
f.write(content + "\n", )
except OSError as reason:
print(str(reason))



def read_file_text_content(file_path):
""" 以文本形式读取文件内容

Args:
file_path (str): 文件路径

Returns:
str: 文件内容
"""
if not os.path.exists(file_path):
return None
else:
with open(file_path, 'r+', encoding='utf-8') as f:
return f.read()


def is_dir_existed(file_path, mkdir=True, is_recreate=False):
""" 判断目录是否存在,不存在则创建

Args:
file_path (str): 文件路径
mkdir (bool): 不存在是否新建
is_recreate (bool): 存在是否删掉重建

Returns:
默认返回None,如果mkdir为False返回文件是否存在
"""
if mkdir:
if not os.path.exists(file_path):
os.makedirs(file_path)
else:
if is_recreate:
delete_file(file_path)
if not os.path.exists(file_path):
os.makedirs(file_path)
else:
return os.path.exists(file_path)

4、效果

执行过程:
image.png
image.png
本机图片:
image.png
image.png
markdown中展示:
image.png

所有文章都替换完成:
image.png

同样的,知乎等其他网站上的文章只要是markdown格式的 图床文件能请求成功的 都可以使用此工具实现下载到本地并替换到文章中去实现本机存储图片永久保留。如果帮助到了你,麻烦点赞收藏评论三连!😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Vmware安装Ubuntu2204实现ssh连接

发表于 2024-04-26

因为公司用的是windows电脑,熟悉下docker技术,就想着安装一个linux虚拟机,vmware使用的是16pro,之前用过centos想着服务器用的ubuntu,ubantu使用提ubuntu-22.04。vmware正常安装好ubuntu后,就是连接不上ssh。把网上的方法都试了个遍,都不得行.

尝试的方法

安装sshd

1
bash复制代码sudo apt-get install openssh-server openssh-client

检查防火墙并放行

1
bash复制代码sudo ufw allow 22

重启sshd

1
sql复制代码systemctl start ssh

查看宿主机和虚拟机网络

1
2
bash复制代码ping www.baidu.com -t #正常
ping 虚拟机ip #正常

我的主机是192.168.2.130 虚拟机设置的是128

image.png
所有检查都正确,就是出现以下错误

image.png

解决办法

打开终端

在终端中,运行以下命令来检查您的网络接口名称:我的是ens33

1
bash复制代码 ip link

image.png

编辑网络配置文件

继续在终端中,运行以下命令来编辑网络配置文件:

1
arduino复制代码 sudo nano /etc/netplan/00-installer-config.yaml

此命令将使用nano文本编辑器打开网络配置文件。

配置静态IP地址

在编辑器中,找到用于您的网络接口的配置部分。例如,如果您的网络接口名称为ens33,则配置部分可能如下所示:

1
2
3
4
5
6
yaml复制代码network:
version: 2
renderer: networkd
ethernets:
ens33:
dhcp4: true

将dhcp4: true修改为dhcp4: false,然后添加addresses和gateway4行来配置静态IP地址和默认网关。假设您的网络网段为192.168.2.0/24,要将Ubuntu 22.04配置为静态IP地址192.168.2.168,默认网关为192.168.21.1,则配置部分应如下所示:

1
2
3
4
5
6
7
8
yaml复制代码network:
version: 2
renderer: networkd
ethernets:
ens33:
dhcp4: false
addresses: [192.168.2.168/24]
gateway4: 192.168.2.1

保存和退出编辑器

完成配置后,按下Ctrl + O保存更改,在按回车键,然后按下Ctrl + X退出nano编辑器。

应用配置更改

在终端中,运行以下命令以应用网络配置更改:

1
bash复制代码 sudo netplan apply

此命令将使您的静态IP地址配置立即生效。

验证静态IP地址配置

最后,运行以下命令来验证您的静态IP地址是否已正确配置:

1
css复制代码ip address show ens33

您将看到类似以下输出:

1
2
3
4
5
6
7
8
9
bash复制代码
rh@ronhai:~$ ip address show ens33
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:ad:53:d9 brd ff:ff:ff:ff:ff:ff
altname enp2s1
inet 192.168.2.168/24 brd 192.168.2.255 scope global ens33
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fead:53d9/64 scope link
valid_lft forever preferred_lft forever

image.png
在输出中,inet行显示您的静态IP地址已成功配置为192.168.2.168。

恭喜!您已成功在Ubuntu 22.04上配置了静态IP地址。现在您的Ubuntu系统将在每次启动时使用您配置的静态IP地址连接到网络。

使用MobaXterm_Personal连接ssh

连接成功
可以使用任何连接工具来连接

image.png

总结三点

  • 检查网络
    看主机和虚拟机是否在同一个网段

我感觉我的问题是互Ping可以但是不能访问就感觉是ip的转发问题,就换了个ip地址,手动配置网络文件来实现,之前是通过网上的教程在vmware上网络配置了桥接后在windows的网络配置以下图的,但是没有用。最好的办法还是通过编辑配置文件来实现

image.png

  • 检查防火墙
    看防火墙是否开启,是否放行端口22
  • 检查sshd
    看是否安装,安装后是否正常启动

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

不同平台配置pip源的几种方案 注意啦,除了在公有云上不然这

发表于 2024-04-26

pip 是 Python 的包管理工具,用于安装和管理 Python 包。如果你想使用清华大学提供的 Python 包镜像源,可以通过修改 pip 的配置文件或者在使用 pip 命令时指定镜像源。以下是两种常用方法:

方法1:修改配置文件(推荐)

对于不同操作系统,pip 的配置文件位置可能不同:

  • 对于 Unix 和 macOS 系统,配置文件通常位于你的主目录下的 .pip/pip.conf(或者 .config/pip/pip.conf)。
  • 对于 Windows 系统,配置文件通常位于 C:\Users\<Username>\pip\pip.ini。

如果这些目录下没有相应的文件,你可以手动创建它们。

这里是配置文件的内容,使用清华大学的镜像源:

1
2
3
4
ini复制代码[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
[install]
trusted-host = pypi.tuna.tsinghua.edu.cn

在 Unix 和 macOS 上,你可以通过以下命令来创建配置文件并写入上述内容:

1
2
3
4
sh复制代码mkdir -p ~/.pip && echo "[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
[install]
trusted-host = pypi.tuna.tsinghua.edu.cn" > ~/.pip/pip.conf

在 Windows 上,你可以手动创建或修改 pip.ini 文件,将上述内容粘贴进去。

方法2:命令行参数

你也可以在使用 pip 命令时通过 --index-url 参数指定使用清华源,例如:

1
sh复制代码pip install -i https://pypi.tuna.tsinghua.edu.cn/simple some-package

这种方法不需要修改配置文件,但你需要在每次使用 pip 命令时都指定镜像源。

永久性修改

如果你想要永久修改 pip 的默认源,应该使用方法1,即修改配置文件。这样,你在使用 pip 安装包时就不需要每次都指定镜像源了。

请注意,由于网络环境的变化,镜像源的URL有可能会发生变化。如果你发现无法访问,请检查清华大学镜像站的官方网站以获取最新的URL。

其他源

注意啦,除了在公有云上不然这里不推荐其他源

在 Python 社区中,除了清华大学的镜像源之外,还有其他一些著名的镜像源可供选择。以下是一些流行的 Python 包镜像源:

  1. 阿里云 (Alibaba Cloud)
1
2
3
4
ini复制代码[global]
index-url = https://mirrors.aliyun.com/pypi/simple/
[install]
trusted-host = mirrors.aliyun.com
  1. 中国科技大学 (University of Science and Technology of China, USTC)
1
2
3
4
ini复制代码[global]
index-url = https://pypi.mirrors.ustc.edu.cn/simple/
[install]
trusted-host = pypi.mirrors.ustc.edu.cn
  1. 豆瓣(Douban)
1
2
3
4
ini复制代码[global]
index-url = https://pypi.doubanio.com/simple/
[install]
trusted-host = pypi.doubanio.com
  1. 华为云 (Huawei Cloud)
1
2
3
4
ini复制代码[global]
index-url = https://mirrors.huaweicloud.com/repository/pypi/simple
[install]
trusted-host = mirrors.huaweicloud.com
  1. 腾讯云 (Tencent Cloud)
1
2
3
4
ini复制代码[global]
index-url = https://mirrors.cloud.tencent.com/pypi/simple
[install]
trusted-host = mirrors.cloud.tencent.com

要使用这些源,你可以按照之前提到的方法修改 pip 配置文件,或者在命令行中使用 --index-url 参数。例如,如果你想使用阿里云的镜像源,可以在命令行中输入:

1
sh复制代码pip install -i https://mirrors.aliyun.com/pypi/simple/ some-package

或者将阿里云的镜像源设置到你的 pip 配置文件中,这样以后所有的 pip 命令都会默认使用这个源。

请注意,镜像站点可能会不定期更改其服务的URL,如果你发现某个镜像源无法使用,请访问对应的官方网站以获取最新的URL信息。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot集成oauth2快速入门demo

发表于 2024-04-26

1.什么是Spring AI?

Spring AI API 涵盖了广泛的功能。 每个主要功能都在其专门的部分中进行了详细介绍。 为了提供概述,可以使用以下关键功能:

  • 跨 AI 提供商的可移植 API,用于聊天、文本到图像和嵌入模型。 支持同步和流 API 选项。 还支持下拉访问模型特定功能。 我们支持 OpenAI、Microsoft、Amazon、Google、Huggingface 等公司的 AI 模型。
  • 跨 Vector Store 提供商的可移植 API,包括同样可移植的新颖的类似 SQL 的元数据过滤器 API。 支持 8 个矢量数据库。
  • 函数调用。 Spring AI 使 AI 模型可以轻松调用 POJO java.util.Function 对象。
  • AI 模型和向量存储的 Spring Boot 自动配置和启动器。
  • 数据工程的 ETL 框架。 这为将数据加载到矢量数据库提供了基础,有助于实现检索增强生成模式,使您能够将数据引入 AI 模型以纳入其响应中。

Chat Completion API

  • 聊天 API 使开发人员能够将人工智能支持的聊天功能集成到他们的应用程序中。 它利用预先训练的语言模型,例如 GPT(生成式预训练变压器),以自然语言对用户输入生成类似人类的响应。
  • API 通常通过向 AI 模型发送提示或部分对话来工作,然后 AI 模型根据其训练数据和对自然语言模式的理解生成对话的完成或延续。 然后,完成的响应将返回到应用程序,应用程序可以将其呈现给用户或将其用于进一步处理。
  • Spring AI Chat Completion API 被设计为一个简单且可移植的接口,用于与各种 AI 模型交互,允许开发人员以最少的代码更改在不同模型之间切换。 这种设计符合 Spring 的模块化和可互换性理念。
  • 此外,在输入封装 Prompt 和输出处理 ChatResponse 等配套类的帮助下,聊天完成 API 统一了与 AI 模型的通信。 它管理请求准备和响应解析的复杂性,提供直接且简化的 API 交互。

2.openapi相关环境准备

参考链接:www.rebelmouse.com/openai-acco…

免费提供api-key

加入博主的知识星球,另外现在加入还可以带大家手把手做一个出海项目,作为项目共创者5c55d703-a577-4942-aa50-f222bf45bcdf

3.代码工程

**

实验目的:实现聊天功能api

**

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
</parent>

<modelVersion>4.0.0</modelVersion>

<artifactId>ai</artifactId>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>0.8.0-SNAPSHOT</version>
</dependency>


</dependencies>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>

</project>

application.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码server:
port: 8088

spring:
ai:
openai:
base-url: https://api.openai.com/
api-key: sk-xxx
embedding:
options:
model: text-davinci-003
chat:
#指定某一个API配置(覆盖全局配置)
api-key: sk-xxx
base-url: https://api.openai.com/
options:
model: gpt-3.5-turbo # 模型配置

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
kotlin复制代码package com.et.ai.controller;

import jakarta.annotation.Resource;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class HelloWorldController {
@Autowired
EmbeddingClient embeddingClient;
@Autowired
ChatClient chatClient;
@GetMapping("/ai/embedding")
public Map embed(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
EmbeddingResponse embeddingResponse = this.embeddingClient.embedForResponse(List.of(message));
return Map.of("embedding", embeddingResponse);
}
@GetMapping("/ai/chat")
public String chat(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
Prompt prompt = new Prompt(message);
return chatClient.call(prompt).getResult().getOutput().getContent();
}
}

DemoApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码package com.et.ai;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

以上只是一些关键代码,所有代码请参见下面代码仓库

代码仓库

  • github.com/Harries/spr…

4.测试

  • 启动Spring Boot应用
  • 访问http://127.0.0.1:8088/ai/chat,返回响应消息

Why couldn’t the bicycle stand up by itself? Because it was two tired!

5.参考引用

  • docs.spring.io/spring-ai/r…
  • www.liuhaihua.cn/archives/71…
  • springboot.io/t/topic/516…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

掌握Go语言:Go语言类型转换,解锁高级用法,轻松驾驭复杂数

发表于 2024-04-26

在Go语言中,类型转换不仅仅局限于简单的基本类型之间的转换,还可以涉及到自定义类型、接口类型、指针类型等的转换。以下是Go语言类型转换的高级用法详解:

Go语言类型转换的高级用法

1. 自定义类型之间的转换

在Go语言中,可以使用类型别名或自定义类型来创建新的数据类型。自定义类型之间的转换需要显示转换,但是可以在逻辑上实现类型的安全转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码package main

import "fmt"

type Celsius float64
type Fahrenheit float64

func main() {
var f Fahrenheit = 100
var c Celsius
c = Celsius((f - 32) * 5 / 9)
fmt.Println("Temperature in Celsius:", c)
}

以上代码演示了在Go语言中自定义类型之间的转换,具体来说,定义了两个自定义类型 Celsius 和 Fahrenheit,分别表示摄氏度和华氏度。然后在 main 函数中,将华氏度转换为摄氏度,并输出结果。

  • 首先,声明了 Celsius 和 Fahrenheit 两个自定义类型,它们分别是 float64 的别名。
  • 在 main 函数中,声明了一个华氏度变量 f 并赋值为 100。
  • 接着,声明了一个摄氏度变量 c。
  • 然后,将 f 转换为摄氏度类型,并将结果赋值给 c,转换的公式是 (f - 32) * 5 / 9。
  • 最后,使用 fmt.Println 输出摄氏度的值。

这段代码展示了如何利用Go语言的类型转换机制,将不同的自定义类型之间的值进行转换,以适应不同的业务需求。

2. 接口类型转换

在Go语言中,接口类型可以存储任意类型的值。当需要从接口类型中取出具体的值时,需要进行类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码package main

import "fmt"

func main() {
var i interface{} = 10
value, ok := i.(int)
if ok {
fmt.Println("Value:", value)
} else {
fmt.Println("Conversion failed")
}
}

以上代码演示了在Go语言中使用类型断言来判断接口类型变量中存储的值的实际类型,并进行相应的类型转换。

  • 在 main 函数中,声明了一个空接口类型变量 i,并将其赋值为整数 10。
  • 然后,使用类型断言 i.(int) 尝试将 i 中的值转换为整数类型,并将结果赋值给 value。
  • 如果类型断言成功(即 i 中的值为整数类型),则 ok 的值为 true,否则为 false。
  • 最后,根据 ok 的值来判断类型转换是否成功,如果成功则输出转换后的整数值,否则输出提示信息 “Conversion failed”。

这段代码展示了如何使用类型断言来动态判断接口类型变量中存储的值的实际类型,并根据需要进行类型转换,以实现更灵活的编程。

3. 指针类型转换

在Go语言中,指针类型之间可以进行转换,但是需要确保目标类型是源类型的子类型或者是相同类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main

import "fmt"

type Animal struct {
Name string
}

type Dog struct {
*Animal
Breed string
}

func main() {
animal := Animal{Name: "Animal"}
dog := Dog{Animal: &animal, Breed: "Labrador"}

fmt.Println("Dog name:", dog.Name)
}

以上代码演示了在Go语言中如何使用结构体嵌套和指针来实现组合关系。

  • 首先,定义了两个结构体类型 Animal 和 Dog。
  • Animal 结构体包含一个字段 Name,用于表示动物的名称。
  • Dog 结构体嵌套了一个指向 Animal 结构体的指针,并拥有自己的字段 Breed,用于表示狗的品种。
  • 在 main 函数中,创建了一个 Animal 类型的变量 animal,并初始化其 Name 字段为 “Animal”。
  • 接着,创建了一个 Dog 类型的变量 dog,通过结构体嵌套将 Animal 结构体作为 Dog 结构体的一个字段,同时指定了 Breed 字段的值为 “Labrador”。
  • 最后,通过 dog.Name 访问了嵌套的 Animal 结构体的 Name 字段,并输出了狗的名称。

这段代码展示了如何在Go语言中使用结构体嵌套和指针来构建复杂的数据结构,并实现了对象之间的组合关系。

应用场景

  1. 数据转换

在处理数据时,可能需要将一种数据类型转换为另一种数据类型,例如将字符串转换为整数、将整数转换为浮点数等。
2. 接口类型断言

当使用接口类型时,可能需要将接口类型断言为具体的类型以进行后续操作,例如从接口类型中取出具体的值进行处理。
3. 指针类型转换

在处理复杂数据结构时,可能需要将指针类型进行转换以获取相关数据或进行操作。

注意事项

  1. 类型断言安全性

在进行类型断言时,需要注意判断断言是否成功,以避免出现panic。

1
2
3
4
5
6
go复制代码var i interface{} = "hello"
if value, ok := i.(int); ok {
fmt.Println("Value:", value)
} else {
fmt.Println("Conversion failed")
}

以上代码演示了在Go语言中进行类型断言时的处理方式。

  • 首先,创建了一个空接口类型 i,并将字符串 “hello” 赋值给它。
  • 接着,在 if 语句中使用了类型断言 i.(int),试图将 i 断言为 int 类型。
  • 如果断言成功,将 i 转换为 int 类型的值,并将其赋值给 value,同时将 ok 设为 true,然后输出转换后的值。
  • 如果断言失败,即 i 的实际类型不是 int,则将 ok 设为 false,表示转换失败,并输出 “Conversion failed”。

由于 i 的实际类型是 string,而不是 int,因此断言失败,最终输出 “Conversion failed”。

  1. 指针类型转换

在进行指针类型转换时,需要确保目标类型是源类型的子类型或者是相同类型,否则可能会导致编译错误或运行时错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码type Animal struct {
Name string
}

type Dog struct {
*Animal
Breed string
}

func main() {
animal := Animal{Name: "Animal"}
dog := Dog{Animal: &animal, Breed: "Labrador"}

fmt.Println("Dog name:", dog.Name)
}

以上代码演示了在Go语言中嵌入结构体的用法。

  • 首先,定义了两个结构体类型:Animal 和 Dog。
  • Dog 结构体嵌入了 Animal 结构体,这意味着 Dog 结构体包含了 Animal 结构体的所有字段和方法。
  • 在 main 函数中,创建了一个名为 animal 的 Animal 类型变量,并初始化其 Name 字段为 “Animal”。
  • 接着,创建了一个名为 dog 的 Dog 类型变量,其中 Animal 字段被赋值为指向 animal 变量的指针,并设置了 Breed 字段为 “Labrador”。
  • 最后,通过 dog.Name 可以访问到 Animal 结构体中的 Name 字段,并输出 “Dog name: Animal”。

这种结构体嵌入的方式可以让 Dog 结构体获得 Animal 结构体的所有属性和方法,实现了代码的复用和组合。

总结

Go语言类型转换的高级用法涉及到自定义类型、接口类型和指针类型的转换,可以在程序中实现复杂数据结构的处理和操作。在进行类型转换时,需要注意类型安全性和转换的合法性,以确保程序的正确性和稳定性。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Next-Admin最佳实践!支持可视化拖拽模块

发表于 2024-04-26

hello,大家好,我是徐小夕,之前和大家分享了很多可视化低代码的最佳实践,以及前端工程化的实战项目,今天继续和大家分享一下我开源的比较有价值的项目——Next-Admin,目前已经支持拖拽搭建模块,并且支持:

  • 参考线吸附
  • 组件成组和取消成组
  • 组件对齐
  • 支持多选,键盘多选
  • 开箱即用的拖拽搭建方案

开源地址:https://github.com/MrXujiang/next-admin

在线demo:http://next-admin.com

往期精彩

  • 独立开发(裸辞)100天,我的阶段性复盘
  • 文档引擎+AI可视化打造下一代文档编辑器
  • 爆肝1000小时, Dooring零代码搭建平台3.5正式上线
  • 从零打造一款基于Nextjs+antd5.0的中后台管理系统

模块演示

drag.gif

技术实现

拖拽模块我采用了 movable, 并研究了它的大量 API,最终实现了我想要的效果,当然我还设计了一套数据结构,如果大家对可视化搭建感兴趣,也可以扩展成自己的拖拽搭建结构。

image.png

元素多选我采用了 selecto 模块,成组管理器我采用了 @moveable/helper, 当然在使用这些库的时候也踩了不少坑,好在已经完美解决。

下面分享一个简单的数据结构,以支持我们的元素自由搭建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
js复制代码const schema = {
"Button": {
id: 'wep_001',
name: 'Button',
type: 'base', // 基础类型组件
base: {
width: 120,
height: 36,
transform: 'translate(100px,100px)'
}
},
"Image": {
id: 'wep_002',
name: 'Image',
type: 'base', // 基础类型组件
base: {
width: 120,
height: 120,
url: '',
transform: 'translate(300px,160px)'
}
}
}

export default schema

工具条实现

image.png

对于工具条的实现,我做了统一的封装,以便后期可能更低成本的维护和管理:

  • config 工具条配置
  • actions 工具条选项对应的功能方法

接下来看看工具条的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
js复制代码const toolbar = {
base: [
{
key: 'group',
icon: <GroupOutlined />,
text: '成组',
},
{
key: 'ungroup',
icon: <UngroupOutlined />,
text: '取消成组'
},
{
key: 'left',
icon: <AlignLeftOutlined />,
text: '左对齐'
},
// ... 其他工具条配置
{
key: 'v-space',
icon: <PicCenterOutlined />,
text: '垂直分布空间'
},
{
key: 'h-space',
icon: <PicCenterOutlined style={{transform: 'rotate(-90deg)'}} />,
text: '水平分布空间'
},

]
}

工具条方法封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
js复制代码const handleOperate = (key: string) => {
// ... some function
// 顶对齐实现
if(key === 'top') {
const rect = moveableRef.current!.getRect();
// console.log(rect)
const moveables = moveableRef.current!.getMoveables();

if (moveables.length <= 1) {
return;
}
moveables.forEach(child => {
child.request<DraggableRequestParam>("draggable", {
y: rect.top,
}, true);
});

moveableRef.current?.updateRect();
return
}
// 底对齐
if(key === 'bottom') {
const rect = moveableRef.current!.getRect();
const moveables = moveableRef.current!.getMoveables();
if (moveables.length <= 1) {
return;
}
moveables.forEach(child => {
child.request<DraggableRequestParam>("draggable", {
y: rect.top + rect.height - (child.props?.target ? (child.props.target as any).offsetHeight : 0),
}, true);
});
moveableRef.current?.updateRect();
return
}

// ... 其他工具条方法
// 水平分布
if(key === 'h-space') {
const groupRect = moveableRef.current!.getRect();
const moveables = moveableRef.current!.getMoveables();
let left = groupRect.left;

if (moveables.length <= 1) {
return;
}
const gap = (groupRect.width - groupRect.children!.reduce((prev, cur) => {
return prev + cur.width;
}, 0)) / (moveables.length - 1);

moveables.sort((a, b) => {
return a.state.left - b.state.left;
});
moveables.forEach(child => {
const rect = child.getRect();

child.request<DraggableRequestParam>("draggable", {
x: left,
}, true);

left += rect.width + gap;
});

moveableRef.current?.updateRect();
return
}

}

通过以上的封装方式我们就能轻松扩展自己的工具条啦~

接下来我们看看工具条实现的效果:

tt.gif

当然代码我已经提交到 github 上了, 大家感兴趣可以参考研究一下。

开源地址:https://github.com/MrXujiang/next-admin

多选 & 成组实现

image.png

下面直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
js复制代码<Selecto
ref={selectoRef}
// dragContainer={container.current}
selectableTargets={[".wep-area .cube"]}
hitRate={0}
selectByClick={true}
selectFromInside={false}
toggleContinueSelect={["shift"]}
ratio={0}
onDragStart={e => {
const moveable = moveableRef.current!;
const target = e.inputEvent.target;
const flatted = deepFlat(targets);

if (
target.tagName === "BUTTON"
|| moveable.isMoveableElement(target)
|| flatted.some(t => t === target || t.contains(target))
) {
e.stop();
}
e.data.startTargets = targets;
}}
onSelect={e => {
const {
startAdded,
startRemoved,
isDragStartEnd,
} = e;

if (isDragStartEnd) {
return;
}
const nextChilds = groupManager.selectSameDepthChilds(
e.data.startTargets,
startAdded,
startRemoved,
);

setSelectedTargets(nextChilds.targets());
}}
onSelectEnd={e => {
const {
isDragStartEnd,
isClick,
added,
removed,
inputEvent,
} = e;
const moveable = moveableRef.current!;

if (isDragStartEnd) {
inputEvent.preventDefault();

moveable.waitToChangeTarget().then(() => {
moveable.dragStart(inputEvent);
});
}
let nextChilds: TargetList;

if (isDragStartEnd || isClick) {
if (isCommand) {
nextChilds = groupManager.selectSingleChilds(targets, added, removed);
} else {
nextChilds = groupManager.selectCompletedChilds(targets, added, removed, isShift);
}

} else {
nextChilds = groupManager.selectSameDepthChilds(e.data.startTargets, added, removed);
}
e.currentTarget.setSelectedTargets(nextChilds.flatten());
setSelectedTargets(nextChilds.targets());
}}
></Selecto>

完整代码都同步到 Next-Admin 了, 如果大家感兴趣也可以研究一下。

后期规划

后续会在 Next-Admin 中集成更多最佳实践,也欢迎感兴趣的朋友一起交流讨论。

如果你对 next 开发或者需要开发一套管理系统, 我相信 Next-Admin 会给你开发和学习的灵感。

同时也欢迎和我一起贡献, 让它变得更优秀~

github地址: https://github.com/MrXujiang/next-admin

演示地址:http://next-admin.com

由于服务器在国外, 所以建议大家git到本地体验~

欢迎star + 反馈~

更多推荐

可视化表单&试卷搭建平台技术详解

爆肝1000小时, Dooring零代码搭建平台3.5正式上线

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…121314…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%