2024年11月

一般的系统登统界面,设计好看一些,系统会增色不少,而常规的桌面程序,包括Web上的很多界面,都借助于背景图片的效果来增色添彩,本篇随笔介绍基于WxPython来做一个登录界面效果,并对系统登录界面在不同系统上(WIndows和MacOS) 进行测试对比,调整最佳的处理方案。

1、登录界面的设计

如前面所讲,我们在登录界面上放置一个图片占位全部,并在合适的位置上添加用户的登录账号和密码输入即可,剩下的就是对登录的请求和响应处理了。

我们基于WxPython来处理,放置图片一般就是用 wx.StaticBitmap,图片我们可以预先把他转换为嵌入的图片对象即可,如我们可以通过 img2py 命令来进行添加,把图片文件生成嵌入图片的对象。

如命令:

img2py -a -F -n quit public/images/quit.ico testimage.py

会在对应的地方生成或者最佳对应的图片内容,如下所示:

我们把它整合在对应的文件中使用即可。

创建一个继承 wx.Frame 但是没有常规对话框的标题框,默认的样式是
style=wx.DEFAULT_FRAME_STYLE

我们不需要标题框,让对话框界面自定义关闭按钮,让它更加好看,因此设置样式
style=wx.FRAME_NO_TASKBAR
下面是对话框的界面效果,我们先看一下。

其中关闭按钮,背景按钮、登录按钮,都是我们使用图片来处理的,按钮采用 wx.BitmapButton 来处理即可。

登录界面的窗体如下所示。

importtestimage as testimageimportwxclassLoginFrame(wx.Frame):def __init__(self, parent, title):
super(LoginFrame, self).
__init__(parent, title=title, size=(600, 375),
style
=wx.FRAME_NO_TASKBAR)
self.InitUI()
defInitUI(self):
panel
=wx.Panel(self)
wx.StaticBitmap(panel,
-1, testimage.login_backimg.Bitmap,
pos
= (0, 0), size=(-1, -1), style=
wx.BORDER_NONE)
font
= wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)

self.userInput
= wx.TextCtrl(panel, -1, value="admin", style=wx.TE_PROCESS_ENTER|wx.TE_CENTER|wx.CENTER, pos=(80, 330), size=(120, 28))
self.userInput.SetHint(
"输入账号")
self.userInput.SetFont(font)

self.passwordInput
= wx.TextCtrl(panel, -1, pos =(280, 330), size=(120, 28), style=wx.TE_PASSWORD| wx.TE_PROCESS_ENTER|wx.TE_CENTER)
self.passwordInput.SetHint(
"输入密码")
self.passwordInput.SetFont(font)

loginButton
=wx.BitmapButton(panel, -1, testimage.login_btn.Bitmap, pos= (410, 328),size= (92, 30))
self.Bind(wx.EVT_BUTTON, self.OnLogin, loginButton)

quitButton
= wx.BitmapButton(panel, -1, testimage.quit.Bitmap, pos= (560, 10),size= (32, 30))
self.Bind(wx.EVT_BUTTON, self.OnQuit, quitButton)

self.Centre()

上面代码,在MacOS上运行界面,还算不错,不过在WIndows上,输入框和标签都无法正常显示,需要主动鼠标单击的时候,才出现,输入焦点也不太正常,后来搜索解决方案才发现是系统界面刷新的问题,不能使用
wx.StaticBitmap
,而需要使用刷新绘制背景图片的方式才可以正常,因此改动一下,使用 EVT_ERASE_BACKGROUND 事件处理的方式绘制背景方式。在初始化界面的时候,绑定该 EVT_ERASE_BACKGROUND 事件,如下代码所示。

self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBack)
    defOnEraseBack(self, event: wx.EraseEvent):
dc
=event.GetDC()if notdc:
dc
=wx.ClientDC(self)
rect
=self.GetUpdateRegion().GetBox()
dc.SetClippingRect(rect)
dc.Clear()
dc.DrawBitmap(testimage.login_backimg.Bitmap, 0, 0)

这样测试MacOS和WIndows均正常显示。

2、登录界面的优化及对接登录处理

稍作调整,在启动背景上添加一些文字显示,最终界面效果如下所示。

具体就是在绘制背景获得ClientDC 对象后绘制文本即可,如下代码所示。

    defOnEraseBack(self, event: wx.EraseEvent):
dc
=event.GetDC()if notdc:
dc
=wx.ClientDC(self)
rect
=self.GetUpdateRegion().GetBox()
dc.SetClippingRect(rect)
dc.Clear()
dc.DrawBitmap(images.login_backimg.Bitmap, 0, 0)

colour
= wx.Colour(52, 94, 150)
dc.SetTextForeground(colour)
dc.SetFont(
wx.Font(
16, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
)
rect
= wx.Rect(10, 210, 580, 30)
dc.DrawLabel(
"基于 wxPython 开发的 GUI 应用", rect, wx.ALIGN_CENTER | wx.ALIGN_TOP
)
rect = wx.Rect(10, 240, 580, 30)
dc.DrawLabel(
"COPYRIGHT © 2024 广州爱奇迪软件科技有限公司",
rect,
wx.ALIGN_CENTER | wx.ALIGN_TOP,
)

为了对接实际的Python开发的FastApi后端,我们对增加一个login.py的API对接类,如下代码所示。

from .base_api importBaseApifrom entity.common importAjaxResponse, AuthenticateResultimportrequestsclassLogin(BaseApi):"""登录处理--API接口类"""api_name= "login"

    def __init__(self):
super().
__init__(self.api_name)def authenticate2(self, json) ->AuthenticateResult:"""同步登录接口

:param json: 登录参数
{
"loginname": "string",
"password": "string",
"systemtype": "string",
}
:return: 登录结果
"""url= f"{self.base_url}/authenticate"data= requests.post(url, json=json).json()

result
=AjaxResponse[AuthenticateResult].model_validate(data)return result.result

增加登录过程的提示处理,代码如下所示。

    defon_login(self, event):self.LoginSync()defLoginSync(self):#print("开始同步登录")
        login_name =self.txtLoginName.GetValue()
login_pwd
=self.txtLoginPwd.GetValue()if login_name == "":
MessageUtil.show_tips(self,
"请输入用户名!")returnjson_data={"loginname": f"{login_name}","password": f"{login_pwd}","systemtype": "WareMis",
}
message= "正在尝试登陆获取令牌,请等待..."icon= get_bitmap("appIcon", 16)
busy
= PBI.PyBusyInfo(message, parent=self, title="登陆处理中...", icon=icon)try:
wx.Yield()
#刷新界面 result=api_login.authenticate2(json_data)ifresult.success:#print("登录成功", result) self._accesstoken =result.accesstoken
ApiClient.set_access_token(result.accesstoken)
self.is_login
=True
self.Close()
else:
MessageUtil.show_tips(self,
"用户名或密码错误!")exceptException as e:#print(e) MessageUtil.show_tips(
self,
"登录失败!错误信息如下:", extended_message=str(e)
)
finally:#关闭提示框 del busy

运行登录后,界面提示获取令牌的信息

成功后跳转到主界面窗体上。

以上就是实际使用wxpython开发跨平台桌面应用的登录界面,对接了后端FastAPI的WebAPI项目,该项目使用 FastAPI, SQLAlchemy, Pydantic, Pydantic-settings, Redis, JWT 构建的项目,数据库访问采用异步方式。 数据库操作和控制器操作,采用基类继承的方式减少重复代码,提高代码复用性。 支持Mysql、Mssql、Postgresql、Sqlite等多种数据库接入,通过配置可以指定数据库连接方式。。

前言

本文介绍一款基于 .NET 6 开发的高级报表工具。该工具支持多种数据源(如数据库、Excel 文件、API 服务等),并具备强大的数据处理和可视化功能。通过内置的集合函数和类 Excel 界面,用户可以轻松设计和生成复杂的报表,满足各类业务需求。

项目介绍

CellReport 是一款专为复杂统计报表设计的工具,支持从数据库、Excel 文件、API 服务等多数据源获取数据,并通过内置的集合函数进行数据组织。其报表设计界面类似 Excel,确保数据展示直观易用。

开发 CellReport 的目的是为了快速制作日常统计报表。通过融合集合运算理念,该工具帮助用户摆脱传统存储过程的束缚,专注于特定指标的加工,并在设计阶段灵活组合数据,大大简化了报表的创建和维护。

项目功能

  • 强大的数据处理能力

CellReport 支持多种数据源接入(如数据库、Excel、CSV 等),并提供内置的数据清洗和转换功能,帮助用户快速准备报表所需数据。

  • 丰富的报表模板

提供多种报表模板(包括柱状图、折线图、饼图、散点图等),通过简单的拖拽和编辑,即可生成专业且美观的统计报表。

  • 智能的数据分析

内置智能数据分析功能,可根据数据特征自动推荐合适的报表类型和可视化方案,同时支持自定义分析公式和算法,增加报表的深度和洞察力。

  • 高效的报表运行

采用先进的计算引擎和缓存技术,即使处理大量数据也能保持高效运行。支持定时任务和数据更新提醒,确保报表数据的实时性和准确性。

项目特点

  • 全面的报表类型:支持常规的清单、分组、交叉报表。 多源与分片:处理多个数据源和分片数据。
  • 数据集运算:支持多数据集的集合运算。
  • 单元格操作:提供单元格扩展和引用功能。
  • 内置脚本引擎:集成类JavaScript语言引擎,支持自定义脚本。
  • 丰富的函数库:提供多种内置函数,支持自定义扩展。
  • 丰富的页面元素:预定义报表组件、ECharts图表组件、数据展示组件等。
  • 灵活的数据引用:报表元素间的数据引用灵活,局部刷新设置便捷。

项目技术

  • 前端设计器
  • 采用 Vue.js、LuckySheet 和 ECharts 等前沿的前端技术,构建了一个基于网页的报表设计环境,提供了直观且功能丰富的用户界面。
  • 后端报表引擎
  • 核心部分基于 .NET 6 开发,实现了报表的高效处理和渲染,支持复杂的报表逻辑和高性能的数据处理能力。

项目效果

1、预览地址

http://20.169.235.199/index.html

测试用户/密码: test/123456

2、部分效果

项目地址

GitHub:
https://github.com/NoneDay/CellReport

Gitee:
https://gitee.com/NoneDay/CellReport

总结

本文示例仅展示了项目工具的部分功能。感兴趣的朋友可以通过项目地址获取更多详细信息。希望本文能在报表开发方面为大家提供有益的帮助。欢迎在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

Open WebUI和Ollama介绍

Open WebUI 是一个功能丰富且用户友好的自托管 Web 用户界面(WebUI),它被设计用于与大型语言模型(LLMs)进行交互,特别是那些由 Ollama 或与 OpenAI API 兼容的服务所支持的模型。Open WebUI 提供了完全离线运行的能力,这意味着用户可以在没有互联网连接的情况下与模型进行对话,这对于数据隐私和安全敏感的应用场景尤为重要。
以下是 Open WebUI 的一些主要特点:

  1. 直观的界面:Open WebUI 的界面受到 ChatGPT 的启发,提供了一个清晰且用户友好的聊天界面,使得与大型语言模型的交互变得直观。
  2. 扩展性:这个平台是可扩展的,意味着可以通过添加新的插件或功能来定制和增强其能力,适应不同的使用场景和需求。
  3. 离线操作:Open WebUI 支持完全离线运行,不依赖于网络连接,适合在任何设备上使用,无论是在飞机上还是在偏远地区。
  4. 兼容性:它兼容多种 LLM 运行器,包括 Ollama 和 OpenAI 的 API,这使得用户可以从多个来源选择和运行不同的语言模型。
  5. 自托管:用户可以在自己的服务器或设备上部署 Open WebUI,这为数据隐私和控制提供了更高的保障。
  6. Markdown 和 LaTeX 支持:Open WebUI 提供了全面的 Markdown 和 LaTeX 功能,让用户可以生成富文本输出,这在科学和学术交流中非常有用。
  7. 本地 RAG 集成:检索增强生成(RAG)功能允许模型利用本地存储的数据进行更深入和具体的回答,增强了聊天交互的功能。

Ollama 是一个开源项目,其主要目标是简化大型语言模型(LLMs)的部署和运行流程,使得用户能够在本地机器或私有服务器上轻松运行这些模型,而无需依赖云服务。以下是 Ollama 的一些主要特点和功能:

  1. 简化部署: Ollama 设计了简化的过程来在 Docker 容器中部署 LLMs,这大大降低了管理和运行这些模型的复杂性,使得非专业人员也能部署和使用。
  2. 捆绑模型组件: 它将模型的权重、配置和相关数据打包成一个被称为 Modelfile 的单元,这有助于优化模型的设置和配置细节,包括 GPU 的使用情况。
  3. 支持多种模型: Ollama 支持一系列大型语言模型,包括但不限于 Llama 2、Code Llama、Mistral 和 Gemma 等。用户可以根据自己的具体需求选择和定制模型。
  4. 跨平台支持: Ollama 支持 macOS 和 Linux 操作系统,Windows 平台的预览版也已经发布,这使得它在不同操作系统上的兼容性更好。
  5. 命令行操作: 用户可以通过简单的命令行指令启动和运行大型语言模型。例如,运行 Gemma 2B 模型只需要执行 ollama run gemma:2b 这样的命令。
  6. 自定义和扩展性: Ollama 的设计允许用户根据特定需求定制和创建自己的模型,这为模型的个性化使用提供了可能。

通过 Ollama,用户可以获得以下好处:

  • 隐私保护:由于模型在本地运行,因此数据不需要上传到云端,从而保护了用户的隐私。
  • 成本节约:避免了云服务的费用,尤其是对于大量请求的情况。
  • 响应速度:本地部署可以减少延迟,提供更快的响应时间。
  • 灵活性:用户可以自由选择和配置模型,以满足特定的应用需求。
    image

我们可以轻松的使用tong2.5和llama3大模型
image

快速使用

阿里云对Open WebUI做了预集成,可以通过链接,完成一键部署
image

部署后可以通过返回的登录地址直接使用.
image

image

简介

image
烂大街的资料不再赘述,简单来说就是
给代码看的注释

Attribute的使用场景

Attribute不仅仅局限于C#中,在整个.NET框架中都提供了非常大的拓展点,任何地方都有Attribute的影子

  1. 编译器层
    比如 Obsolete,Conditional
  2. C#层
    GET,POST,Max,Range,Require
  3. CLR VM层
    StructLayout,DllImport
  4. JIT 层
    MethodImpl

Attribute在C#中的调用

举个常用的例子,读取枚举上的自定义特性。

    public enum Test
    {
        [EnumDescription("hhhhhh")]
        None = 0,
        [EnumDescription("xxxxxx")]
        Done =1
    }
	private static IEnumerable<string> GetEnumDescriptions(this Enum e)
	{
		IEnumerable<string> result = null;
        var type = e.GetType();
        var fieldInfo = type.GetField(e.ToString());
        var attr = fieldInfo?.GetCustomAttributes(typeof(EnumDescriptionAttribute), false);
        if (attr?.Length > 0)
        {
			result = attr.Cast<EnumDescriptionAttribute>().Select(x => x.Description);
        }
		return result ?? Enumerable.Empty<string>();
	}

可以看到,Attribute底层在C#中实现依旧是依赖反射,所以为什么说Attribute是
写给代码看的注释
,因此对反射的优化思路也可以用在Attribute中。
比如在代码中,使用Dictionary缓存结果集。避免过多调用反射造成的性能问题。

        private static IEnumerable<string> GetEnumDescriptionsCache(this Enum e)
        {
            var key = $"{e.GetType().Name}_{e.ToString()}";
            if (_enumMap.ContainsKey(key))
            {
                return _enumMap[key];
            }
            else
            {
                var result = GetEnumDescriptions(e);
                _enumMap.TryAdd(key, result);
                return result;
            }
        }

循环100000次造成的性能差距还是很明显的
image

Newtonsoft.Json对Attrubute的使用

以JsonConverter为蓝本举例说明。

    public class Person
    {
        [JsonConverter(typeof(DateTimeConverter))]
        public DateTime CreateTime { get; set; }
    }
	public class DateTimeConverter : JsonConverter<DateTime>
    {
        public override DateTime ReadJson(JsonReader reader, Type objectType, DateTime existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            if (reader.Value == null)
                return DateTime.MinValue;

            if (DateTime.TryParse(reader.Value.ToString(), out DateTime result))
                return result;

            return DateTime.MinValue;
        }

        public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer)
        {
            writer.WriteValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
        }
    }

定义了一个Attribute:JsonConverter.其底层调用如下:

        [RequiresUnreferencedCode(MiscellaneousUtils.TrimWarning)]
        [RequiresDynamicCode(MiscellaneousUtils.AotWarning)]
        public static JsonConverter? GetJsonConverter(object attributeProvider)
        {
			// 底层还是调用Reflection,为了性能,也缓存了对象元数据。
            JsonConverterAttribute? converterAttribute = GetCachedAttribute<JsonConverterAttribute>(attributeProvider);

            if (converterAttribute != null)
            {
                Func<object[]?, object> creator = CreatorCache.Instance.Get(converterAttribute.ConverterType);
                if (creator != null)
                {
                    return (JsonConverter)creator(converterAttribute.ConverterParameters);
                }
            }

            return null;
        }

https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs

Attribute在CLR上的调用

    public class NativeMethods
    {
        [DllImport("xxxxx", EntryPoint = "add", CallingConvention = CallingConvention.Cdecl)]
        public extern static int ManagedAdd(int a, int b);
    }

在CLR中,同样用来调用 C/C++ 的导出函数。有兴趣的朋友可以使用windbg查看线程调用栈。以及在MetaData中有一张ImplMap表,存储着C#方法与C++函数的mapping关系

Attribute在JIT上的调用

    public class Person
    {
        public int id { get; set; } = 0;

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void SyncMethod()
        {
            id++;
        }
    }

JIT会自动为该Attribute注入同步代码
image
image

其本质就是注入lock同步块代码,只是颗粒度在整个方法上。相对比较大

结论

Attrubute在C#层面,底层使用反射。因此使用自定义Attribute时,酌情使用缓存来提高性能


Minio 是一个基于Apache License v2.0开源协议的对象存储服务。它可以运行在多种操作系统上,包括 Linux 和 Windows 等。
它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5TB不等。

Minio官网:
https://min.io/
中文地址:
https://www.minio.org.cn
中文文档:
https://www.minio.org.cn/docs/minio/linux/index.html
演示:
https://play.minio.org.cn
用户名:minioadmin、密码minioadmin

MinIO Linux 安装

单节点部署

Download:
https://www.minio.org.cn/download.shtml#/linux
使用以下命令下载安装最新版本的稳定 MinIO二进制包, 并设置 $PATH :

[root@localhost ~]# wget https://dl.minio.org.cn/server/minio/release/linux-amd64/minio
[root@localhost ~]# chmod +x minio
[root@localhost ~]# sudo mv minio /usr/local/bin/

image

创建 systemd 系统启动服务文件

创建 minio.service 启动文件,确保文件在
/usr/lib/systemd/system/minio.service

[root@localhost ~]# vi /usr/lib/systemd/system/minio.service

minio.service

[Unit]
Description=MinIO
Documentation=https://min.io/docs/minio/linux/index.html
Wants=network-online.target
After=network-online.target
AssertFileIsExecutable=/usr/local/bin/minio

[Service]
WorkingDirectory=/usr/local

User=minio-user
Group=minio-user
ProtectProc=invisible

EnvironmentFile=-/etc/default/minio
ExecStartPre=/bin/bash -c "if [ -z \"${MINIO_VOLUMES}\" ]; then echo \"Variable MINIO_VOLUMES not set in /etc/default/minio\"; exit 1; fi"
ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES

# MinIO RELEASE.2023-05-04T21-44-30Z adds support for Type=notify (https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=)
# This may improve systemctl setups where other services use `After=minio.server`
# Uncomment the line to enable the functionality
# Type=notify

# Let systemd restart this service always
Restart=always

# Specifies the maximum file descriptor number that can be opened by this process
LimitNOFILE=65536

# Specifies the maximum number of threads this process can create
TasksMax=infinity

# Disable timeout logic and wait until process is stopped
TimeoutStopSec=infinity
SendSIGKILL=no

[Install]
WantedBy=multi-user.target

# Built for ${project.name}-${project.version} (${project.name})

默认情况下,
minio.service
文件以
minio-user
用户和组的身份运行。 您可以使用
groupadd

useradd
创建用户和组。 命令。下面的示例创建了用户和组,并设置了权限 来访问供 MinIO 使用的文件夹路径。这些命令通常 需要 root ( sudo ) 权限。

[root@localhost ~]# groupadd -r minio-user
[root@localhost ~]# useradd -M -r -g minio-user minio-user
# 存储路径
[root@localhost ~]# chown minio-user:minio-user /mnt/data
chown: 无法访问"/mnt/data": 没有那个文件或目录
[root@localhost ~]# mkdir /mnt/data
[root@localhost ~]# chown minio-user:minio-user /mnt/data

本例中的驱动器路径由 MINIO_VOLUMES 环境变量指定。更改此处和环境变量文件中的值,使其与 MinIO 打算使用的驱动器路径相匹配。

创建环境变量文件


/etc/default/minio
创建环境变量文件。 MinIO 服务器容器可以将此文件作为所有
environment variables
下面的示例提供了一个起始环境文件:
密码不能小于8个字符, 否则无法启动

[root@localhost ~]# vi /etc/default/minio
# MINIO_ROOT_USER and MINIO_ROOT_PASSWORD sets the root account for the MinIO server.
# This user has unrestricted permissions to perform S3 and administrative API operations on any resource in the deployment.
# Omit to use the default values 'minioadmin:minioadmin'.
# MinIO recommends setting non-default values as a best practice, regardless of environment
# 用户名长度不能小于3个字符
MINIO_ROOT_USER=admin
# 密码不能小于8个字符, 否则无法启动
MINIO_ROOT_PASSWORD=minioadmin

# MINIO_VOLUMES sets the storage volume or path to use for the MinIO server.

MINIO_VOLUMES="/mnt/data"

# MINIO_OPTS sets any additional commandline options to pass to the MinIO server.
# For example, `--console-address :9001` sets the MinIO Console listen port
MINIO_OPTS="--console-address :9001"

启动MinIO服务

启动 MinIO SNSD 部署即服务:

[root@localhost ~]# sudo systemctl start minio.service

使用以下命令确认服务是否在线和功能正常:

[root@localhost ~]# sudo systemctl status minio.service
[root@localhost ~]# journalctl -f -u minio.service

自启动,将进程作为主机引导的一部分,在服务器重启的过程中该进程会自动重启,而不用再进行手动管理。

[root@localhost ~]# sudo systemctl enable minio.service

image

连接到MinIO服务

浏览器中输入:
http://localhost:9001
登录MinIO的用户名和密码配置参数为
MINIO_ROOT_USER

MINIO_ROOT_PASSWORD
这些配置可以在在容器指定的环境文件中进行修改。
image
image
您可以使用MinIO控制台进行一般管理任务,如身份和访问管理、指标和日志监控或服务器配置。每个MinIO服务器都包含其自己的嵌入式MinIO控制台。 如果您的本地主机防火墙允许外部访问控制台端口,则同一网络上的其他主机可以使用您的本地主机的IP地址或主机名访问控制台。

SpringBoot项目整合MinIO

https://mvnrepository.com/artifact/io.minio/minio

配置项

添加依赖

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.10</version>
</dependency>

application.yml 配置 minio


spring:
     mvc:
      hiddenmethod:
        filter:
          enabled: true
  #设置文件上传大小限制
  servlet:
    multipart:
      max-file-size: -1    #设置单个文件的大小 -1表示不限制
      max-request-size: -1    #单次请求的文件的总大小 -1表示不限制

minio:
  endpoint: http://172.16.3.195:9000
  accessKey: admin
  secretKey: minioadmin
  bucketName: vipsoft-devminioadmin

MinioConfig.java

package com.vipsoft.oss.config;


import io.minio.MinioClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
    /**
     * 服务地址:http://172.16.3.195:9000
     */
    private String endpoint;

    /**
     * 用户名
     */
    private String accessKey;

    /**
     * 密码
     */
    private String secretKey;

    /**
     * 存储桶名称
     */
    private String bucketName;


    @Bean
    public MinioClient getMinioClient() {
        MinioClient minioClient = MinioClient.builder().endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
        return minioClient;
    }

    //todo 省去 getter & setter
}

工具类

MinioUtil.java

package com.vipsoft.oss.util;

import cn.hutool.core.io.IoUtil;
import com.cuwor.oss.config.MinioConfig;
import io.minio.*;
import io.minio.MinioClient;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
public class MinioUtil {

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MinioConfig minioConfig;

    private static final int DEFAULT_EXPIRY_TIME = 7 * 24 * 3600;

    /**
     * 判断bucket是否存在
     */
    public boolean bucketExists(String bucketName) {
        boolean exists;
        try {
            exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            exists = false;
        }
        return exists;
    }


    /**
     * 创建存储桶,存在则不创建
     */
    public boolean makeBucket(String bucketName) {
        boolean exists;
        try {
            exists = bucketExists(bucketName);
            if (!exists) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
                exists = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
            exists = false;
        }
        return exists;
    }

    /**
     * 删除存储桶 -- 有文件,不让删除
     *
     * @param bucketName 存储桶名称
     * @return boolean
     */
    public boolean removeBucket(String bucketName) {
        try {
            boolean flag = bucketExists(bucketName);
            if (flag) {
                Iterable<Result<Item>> myObjects = listObjects(bucketName);
                for (Result<Item> result : myObjects) {
                    Item item = result.get();
                    // 有对象文件,则删除失败
                    if (item.size() > 0) {
                        return false;
                    }
                }
                // 删除存储桶,注意,只有存储桶为空时才能删除成功。
                minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
                flag = bucketExists(bucketName);
                if (!flag) {
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 列出存储桶中的所有对象
     *
     * @param bucketName 存储桶名称
     * @return Iterable<Result < Item>>
     */
    public Iterable<Result<Item>> listObjects(String bucketName) {
        boolean flag = bucketExists(bucketName);
        if (flag) {
            return minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());
        }
        return null;
    }


    /**
     * 列出所有存储桶名称
     *
     * @return List<String>
     */
    public List<String> listBucketNames() {
        List<String> bucketListName = new ArrayList<>();
        try {
            List<Bucket> bucketList = minioClient.listBuckets();
            for (Bucket bucket : bucketList) {
                bucketListName.add(bucket.name());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bucketListName;
    }

    /**
     * 列出存储桶中的所有对象名称
     *
     * @param bucketName 存储桶名称
     * @return List<String>
     */
    public List<String> listObjectNames(String bucketName) {
        List<String> listObjectNames = new ArrayList<>();
        try {
            boolean flag = bucketExists(bucketName);
            if (flag) {
                Iterable<Result<Item>> myObjects = listObjects(bucketName);
                for (Result<Item> result : myObjects) {
                    Item item = result.get();
                    listObjectNames.add(item.objectName());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return listObjectNames;
    }


    /**
     * 获取对象的元数据
     *
     * @param objectName 存储桶里的对象名称
     * @return
     */
    public StatObjectResponse statObject(String objectName) {
        StatObjectResponse statObject = null;
        try {
            statObject = minioClient.statObject(StatObjectArgs.builder().bucket(minioConfig.getBucketName()).object(objectName).build());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return statObject;
    }

    //region 上传文件

    /**
     * 上传文件
     *
     * @param file       文件
     * @param objectName 文件名称
     */
    public boolean uploadObject(MultipartFile file, String objectName) {
        // 使用putObject上传一个文件到存储桶中。
        try {
            InputStream inputStream = file.getInputStream();
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .stream(inputStream, file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build());
            IoUtil.close(inputStream);
//            return StrUtil.format("{}/{}/{}", minioConfig.getEndpoint(), minioConfig.getBucketName(), objectName);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 通过InputStream上传对象
     *
     * @param objectName  存储桶里的对象名称
     * @param inputStream 要上传的流
     * @param contentType 上传的文件类型 例如 video/mp4  image/jpg
     * @return boolean
     */
    public boolean uploadObject(InputStream inputStream, String objectName, String contentType) {
        try {
            minioClient.putObject(PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(objectName).stream(
                            //不清楚文件的大小时,可以传-1,10485760。如果知道大小也可以传入size,partsize。
                            inputStream, -1, 10485760)
                    .contentType(contentType)
                    .build());
            IoUtil.close(inputStream);
            StatObjectResponse statObject = statObject(objectName);
            if (statObject != null && statObject.size() > 0) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 通过文件上传到对象
     *
     * @param objectName 存储桶里的对象名称
     * @param fileName   File name
     * @return boolean
     */
    public boolean uploadObject(String objectName, String fileName) {
        try {
            minioClient.uploadObject(UploadObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .filename(fileName)
                    .build());
            StatObjectResponse statObject = statObject(objectName);
            if (statObject != null && statObject.size() > 0) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;
    }

    //endregion


    /**
     * 获取文件访问地址
     *
     * @param fileName 文件名称
     */
    public String getPresignedObjectUrl(String fileName) {
        try {
            return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(minioConfig.getBucketName())
                    .object(fileName)
                    .build()
            );
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getObjectUrl(String objectName) {
        String url = "";
        try {
            url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .build());

        } catch (Exception e) {
            e.printStackTrace();
        }
        return url;
    }

    /**
     * 生成一个给HTTP GET请求用的presigned URL。
     * 浏览器/移动端的客户端可以用这个URL进行下载,即使其所在的存储桶是私有的。这个presigned URL可以设置一个失效时间,默认值是7天。
     *
     * @param objectName 存储桶里的对象名称
     * @param expires    失效时间(以小时单位),默认是7天,不得大于七天
     * @return
     */
    public String getObjectUrl(String objectName, Integer expires) {
        String url = "";
        try {
            if (expires < 1 || expires > DEFAULT_EXPIRY_TIME) {
                throw new Exception("Expires must be in range of 1 to " + DEFAULT_EXPIRY_TIME);
            }
            url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .expiry(expires, TimeUnit.HOURS)//动态参数
                    // .expiry(24 * 60 * 60)//用秒来计算一天时间有效期
                    // .expiry(1, TimeUnit.DAYS)//按天传参
                    // .expiry(1, TimeUnit.HOURS)//按小时传参数
                    .build());

        } catch (Exception e) {
            e.printStackTrace();
        }
        return url;
    }

    /**
     * 下载文件,通过 HttpServletResponse 返回
     *
     * @param objectName 存储桶里的对象名称
     */
    public void downloadObject(HttpServletResponse response, String objectName) {
        try {
            InputStream file = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .build());
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(objectName, "UTF-8"));
            ServletOutputStream servletOutputStream = response.getOutputStream();
            int len;
            byte[] buffer = new byte[1024];
            while ((len = file.read(buffer)) > 0) {
                servletOutputStream.write(buffer, 0, len);
            }
            servletOutputStream.flush();
            file.close();
            servletOutputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 下载并将文件保存到本地
     *
     * @param objectName 存储桶里的对象名称
     * @param fileName   下载保存的文件名
     * @return boolean
     */
    public boolean downloadObject(String objectName, String fileName) {
        try {
            StatObjectResponse statObject = statObject(objectName);
            if (statObject != null && statObject.size() > 0) {
                minioClient.downloadObject(DownloadObjectArgs.builder()
                        .bucket(minioConfig.getBucketName())
                        .object(objectName)
                        .filename(fileName)
                        .build());
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }


    /**
     * 以流的形式获取一个文件对象
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @return InputStream
     */
    public InputStream getObject(String bucketName, String objectName) {
        try {
            StatObjectResponse statObject = statObject(objectName);
            if (statObject != null && statObject.size() > 0) {
                InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
                return stream;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 以流的形式获取一个文件对象(断点下载)
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @param offset     起始字节的位置
     * @param length     要读取的长度 (可选,如果无值则代表读到文件结尾)
     * @return InputStream
     */
    public InputStream getObject(String bucketName, String objectName, long offset, Long length) {
        try {
            StatObjectResponse statObject = statObject(objectName);
            if (statObject != null && statObject.size() > 0) {
                InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .offset(1024L)
                        .length(4096L)
                        .build());
                return stream;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 删除一个对象
     *
     * @param objectName 存储桶里的对象名称
     */
    public boolean removeObject(String objectName) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioConfig.getBucketName()).object(objectName).build());
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表
     *
     * @param bucketName  存储桶名称
     * @param objectNames 含有要删除的多个object名称的迭代器对象 eg:
     *                    List<DeleteObject> objects = new LinkedList<>();
     *                    objects.add(new DeleteObject("my-objectname1"));
     *                    objects.add(new DeleteObject("my-objectname2"));
     *                    objects.add(new DeleteObject("my-objectname3"));
     * @return 如果有值,说明当前文件删除失败
     */
    public List<String> removeObjects(String bucketName, List<DeleteObject> objectNames) {
        List<String> deleteErrorNames = new ArrayList<>();
        try {
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(objectNames).build());
            for (Result<DeleteError> result : results) {
                DeleteError error = result.get();
                deleteErrorNames.add(error.objectName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return deleteErrorNames;
    }
}


测试

package com.vipsoft.admin;

import cn.hutool.core.util.StrUtil;
import com.cuwor.oss.util.MinioUtil;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;

@SpringBootTest
public class MinioTest {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private MinioUtil minioUtil;


    @Test
    void makeBucketTest() {
        boolean flag = minioUtil.makeBucket("public");
        Assert.isTrue(flag, "查询异常");
    }


    @Test
    void uploadObjectTest() {
        boolean flag = minioUtil.uploadObject("/avatar/123.png", "D:\\Users\\Pictures\\R-C.png");
        Assert.isTrue(flag, "上传异常");
    }


    @Test
    void getObjectUrlTest() {
        String objectName="/avatar/123.png";
        String url = minioUtil.getObjectUrl(objectName);
        logger.info("url:{}", url);
        url = minioUtil.getObjectUrl(objectName, 3);
        logger.info("expires url:{}", url);
        Assert.isTrue(StrUtil.isNotEmpty(url), "上传异常");
    }
}

image