2024年1月

1、准备材料

开发板(
正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

野火DAP仿真器

keil µVision5 IDE(
MDK-Arm

ST-LINK/V2驱动

XCOM V2.6串口助手

逻辑分析仪
nanoDLA

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板的
独立看门狗(IWDG)和窗口看门狗(WWDG)并了解其功能实现

3、WWDG实验流程

3.0、前提知识

STM32F407拥有两个看门狗,分别为独立看门狗和窗口看门狗,这两个看门狗的作用都是监控程序运行,程序死机或跑飞就使系统复位,值得注意的是,一旦开启看门狗就无法停止,除非系统复位

窗口看门狗的结构不复杂,其结构框图上描述了有关窗口看门狗的所有内容,首先WDG的时钟来自于PCLK1(STM32F407最高42MHz)的4096次分频后再进行可选的1/2/4/8分频,其内部有一个7位的递减计数器T[6:0],还有一个WWDG_CFR配置寄存器,该寄存器位6:0包含了用于与递减计数器进行比较的窗口值W[6:0]

当出现以下两种情况时会产生窗口看门狗复位操作

  1. 当看门狗激活(WDGA=1)且T6位由1变为0时(也即T[6:0]<0X3F时)

  2. 当看门狗激活且T[6:0]>W[6:0]时用户写WWDG_CR寄存器的值(也即刷新T[6:0]值)

如下所示为窗口看门狗逻辑框图
(注释1)

通过上述的描述,
可知窗口看门狗要想不产生复位操作,必须将其计数器的计数值控制在W[6:0]和0x3F之间才能喂狗(刷新T[6:0]值)
,当 “计数器的值小于0x3F” 或者 “当计数器的值T[6:0]>W[6:0]时用户喂狗” 都会产生Reset操作,这也是为什么叫窗口看门狗的原因,如下图所示为窗口看门狗的工作时序图(注释1)

窗口看门狗可设置的参数只有三个

WWDG counter clockprescaler
(时钟分频):可以设置为1/2/4/8分频

WWDG window value
(窗口W[6:0]值):可设置范围T[6:0]~127

WWDG down-counter reload value
(递减计数器重装值):可设置范围63-127(7位最大值127且不能小于0x3F)

注意本实验由于独立看门狗较为简单,所以选择将独立/窗口看门狗放在一篇文章里,但是读者复现实验的时候应该分开做,不要同时启用窗口看门狗和独立看门狗(不是不可以同时启用,而是对于本实验例子同时启用不方便理解)

另外需要提醒的是,
独立/窗口看门狗只要被初始化就会自动启动,用户只需喂狗即可

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号)选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“
STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

由于窗口看门狗递减定时器为7位且递减到0x3F就会产生复位,因此最大计数值也才127-63=64,因此其时钟频率尽可能低一点,否则超时时间太短,实验现象不太好观察,这里将PCLK1设置为了21MHz,具体时钟配置如下图所示

3.1.2、外设参数配置

本实验需要初始化开发板上GREEN_LED和RED_LED,具体配置步骤请阅读“
STM32CubeMX教程2 GPIO输出 - 点亮LED灯

本实验需要初始化开发板上KEY2用户按键,具体配置步骤请阅读“
STM32CubeMX教程3 GPIO输入 - 按键响应

本实验需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信

单击Pinout & Configuration页面左边的System Core的WWDG进入窗口看门狗的设置,在Mode中勾选Activated,在下方配置其分频系数为8,窗口值为90,计数器重装值为最大值127,具体配置如下图所示

现在我们来计算一下窗口看门狗的超时时间

首先PCLK1设置为了21MHz,然后该时钟经过4096次分频,在经过设置的8分频得到的频率为640.869140625Hz

则其超时时间可以计算得到为(127-63)/640.869140625≈0.09986s≈99.86ms

其不允许喂狗的时间可以计算得到为(127-90)/640.869140625≈57.734ms

即当窗口定时器被初始化启动后,如果在57.734ms-100ms之间没有进行喂狗操作,那么窗口看门狗将超时导致程序复位

3.1.3、外设中断配置

在Pinout & Configuration页面左边System Core/NVIC中勾选Window watchdog interrupt全局中断,然后选择合适的中断优先级即可

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节

3.2.1、外设初始化调用流程

在生成的工程主函数main()中调用MX_WWDG_Init()函数对WWDG参数配置,并调用了HAL_WWDG_Init()初始化函数

在HAL_WWDG_Init()函数中对WWDG做了初始化,并调用了HAL_WWDG_MspInit()函数对WWDG的时钟、中断优先级和中断使能做了配置

WWDG具体初始化调用流程如下图所示

3.2.2、外设中断调用流程

在STM32CubeMX中勾选了WWDG的中断后,会在stm32f4xx_it.c文件中出现串口看门狗中断服务函数WWDG_IRQHandler()

该中断服务函数WWDG_IRQHandler()调用了HAL库WWDG中断统一管理函数HAL_WWDG_IRQHandler()

最终在HAL_WWDG_IRQHandler()函数中调用了早期唤醒回调函数HAL_WWDG_EarlyWakeupCallback(),该函数为虚函数,需要用户重新实现

WWDG具体中断调用流程如下图所示

3.2.3、添加其他必要代码

在wwdg.c中重新实现早唤醒回调函数HAL_WWDG_EarlyWakeupCallback(),当递减计数器递减到0x40,下次递减即将产生看门狗复位时,就会进入到该早唤醒回调函数中,但是该函数只能执行一个时钟节拍,然后系统就复位了,因此该函数内部执行不了复杂的程序,笔者这里也只是输出了一个字符‘1’,源代码如下

/*早唤醒回调函数*/
void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg)
{
    printf("1\r\n");
}

在主函数中初始化完毕拉低PF9引脚,主循环中延时喂狗,并闪烁GREEN_LED,当系统喂狗及时,用户就可以看到GREEN_LED闪烁,当喂狗不及时,系统反复复位,就看不到GREEN_LED闪烁

源代码如下

/*主循环外代码*/
printf("Reset\r\n");
HAL_GPIO_WritePin(RED_LED_GPIO_Port,RED_LED_Pin,GPIO_PIN_RESET);
 
 
/*主循环内代码*/
HAL_Delay(70);
//HAL_Delay(40);
HAL_WWDG_Refresh(&hwwdg);
HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port,GREEN_LED_Pin);

4、常用函数

/*独立看门狗喂狗函数*/
HAL_StatusTypeDef HAL_IWDG_Refresh(IWDG_HandleTypeDef *hiwdg)
 
/*窗口看门狗喂狗函数*/
HAL_StatusTypeDef HAL_IWDG_Refresh(IWDG_HandleTypeDef *hiwdg)
 
/*窗口看门狗早唤醒中断回调函数*/
void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg)

5、烧录验证

将程序修改为延时110ms,烧录程序,单片机上电后通过串口可以发现每隔一段时间都会发生进入早唤醒中断和复位操作,此时开发板上只有RED_LED被点亮,GREEN_LED呈现熄灭状态,使用逻辑分析仪捕获PF9引脚的电平,可以发现复位间隔时间大概为99ms,与我们设置预期的99.864ms大致一致,如下图所示为串口输出数据和逻辑分析仪捕获PF9引脚电平图

使用PF9引脚的状态来判断复位时间间隔不太严谨,因此我们将WWDG的参数做修改二次测试,分频不变仍为8,窗口值修改为80,重装值修改为100,此时通过计算可知递减到63大约需要57.734ms,将程序修改为延时70ms,然后使用逻辑分析仪再次捕获PF9引脚的电平,可以发现复位间隔时间大概为57ms,经过这两次测试,可知虽然不严谨但可以验证,如下图所示为逻辑分析仪捕获的PF9引脚电平图

WWDG复原回原来参数,分频8,窗口值90,重装值127,将程序修改为延时70ms,然后烧录程序,单片机上电后通过串口可以发现不会频繁的输出复位信息,也不会进入早唤醒中断回调函数中,此时开发板上RED_LED被点亮,GREEN_LED呈现闪烁状态,证明喂狗比较及时,没有产生窗口看门狗复位

6、IWDG实验流程简述

6.0、前提知识

独立看门狗很简单,本质就是一个12位的递减计数器
,当递减到0之后就产生看门狗复位操作,
其时钟来源于单片机内部的LSI RC(32KHz)且不可更换
,该内部RC震荡产生的时钟信号频率误差较大,因此在设置看门狗复位时间时最好留有余量,如下图所示为其时钟来源

独立看门狗可设置的参数只有两个

IWDG counter clockprescaler
(时钟分频):可以设置为4/8/16/.../256分频

IWDG down-counter reload value
(递减计数器重装值):可设置范围0-4095(12位)

当其重装值设置为最大值4095,根据时钟分频的不同,看门狗的超时时间也不同,最长可达32.768s,最短只有0.512s,具体超时时间如下表所示

6.1、CubeMX相关配置

只需在Pinout & Configuration页面左边功能分类栏目System Core中单击其中IWDG,在Mode中勾选Activated,最后在下方配置两个参数即可,时钟使用LSI配置不可调,IWDG无中断,具体步骤如下图所示

6.2、添加其他必要代码

程序初始化完毕后拉低PF9引脚,在主循环中实现按键响应,按下KEY2按键执行喂狗操作

源代码如下

/*主函数初始化完后将PF9引脚拉低*/
HAL_GPIO_WritePin(RED_LED_GPIO_Port,RED_LED_Pin,GPIO_PIN_RESET);
 
/*主循环中程序/
/*按键KEY2被按下*/
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
    {
        printf("Now feed the dog\r\n");
        HAL_IWDG_Refresh(&hiwdg);
        while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin));
    }
}

6.3、实验现象

烧录程序,单片机上电后通过串口可以发现每隔一段时间都会发生复位操作,当按下KEY2按键在该间隔时间内喂狗,就不会产生复位操作,而一旦停止喂狗,超时后又会产生复位操作,具体如下图所示

当单片机复位后会首先执行一系列初始化操作,然后手动将PF9引脚设置为低电平(该引脚默认为高),直到由于没有喂狗产生看门狗复位为止,通过逻辑分析仪捕获PF9引脚的电平,可以发现其两次复位间隔时间与我们所设置预期的看门狗超时时间大致一致,具体如下图所示

7、注释详解

注释1
:图片来源于 STM32F4xx 参考手册 RM0090

POS软件是什么?你好意思吗,还在用老掉牙的Winform。

关于POS

销售终端——POS(point of sale)是一种多功能终端,把它安装在信用卡的特约商户和受理网点中与计算机联成网络,就能实现电子资金自动转账,它具有支持消费、预授权、余额查询和转账等功能,使用起来安全、快捷、可靠。

POS软件大都长这样子:

不好意思了,我没有专门去收集POS软件的UI界面。感兴趣的朋友可以去澳大利亚知名POS软件供应商IDEALPOS的官网去围观:

POS Systems Australia - Software & Hardware - Idealpos

前言

好久以前就想写这篇文章了,一是想要证明我还在程序员队伍里苟延残喘。二是这样的文章我从来就没看见过,写出来万一帮到其他人了呢。今天早上我查看请求的状态时豁然发现已经通过平台的审核了,接下来就是区域的审核:我们公司在FEIJI和SAMOA都有门店。

目前TaxCore提供的税务平台有四个国家(地区)在用:
Samoa ,Fiji,USA, WA.
目前TaxCore上备案通过的也没几家公司,我知道的就只要上面提到的澳大利亚IDEALPOS。

我的软件界面有参考它。在国内税务发票是使用针式打印机,套打发票,而国外的发票要求没这么高,用普通的热敏打印机打印就可以了,因为发票是可以在TaxCore的网站上面看到的。所以不能长期保存也没有问题。
下面展示一张财务发票:
TaxCore: Invoice Verification - TaxCore
,
点击一下页面的
Journal
超链接能看到财政发票的原始样子,大概就是这样子

对于纳税人来说可以通过扫描发票上的二维码进行在线查看。对于商户来说,也可以进入到自己所在区域的税所的门户网站后台查看每一张发票,这是TaxCore的全球的沙盒环境的门户:
TaxCore | Log off
。财政发票的从开始到结束部分是不运行有任何改动的。但商家可以在头尾加一点自己的东西比如:

TaxCore的审核是我见过最认真细致的,对发票的要求细致得很。我们软件来来回回经过十几轮审核修好终于才通过了。下面展示TaxCore对发票的要求:

乍一看,这基本上是不可能完成的任务吧,全英文的在线文档,没有人带,还好官方有个工单系统,有问题可以在上面提问,会得到很及时的很友好的回复(由于一般有时区的差异,工作时间不一样不要要求马上有答复)。关于官方的帮助文档的地址请点击这里:
TaxCore - Help Viewer (revenue.gov.ws)

指北的正文

<!DOCTYPE html>
<html>
<head>
    <title>Online POS</title>
    <metacharset="utf-8">
</head>
<body>
    <h1>Online POS</h1>
    <labelfor="invoiceRequest">Invoice request json</label>
    <textareaid="invoiceRequest"cols="100"rows="30"style="display:block"></textarea>
    <labelfor="taxcore_sign_element">Send Invoice Request:</label>
    <!--TaxCore HTML element-->
    <buttonid="taxcore_sign_element"data-taxcore-vsdc-url=" https://vsdc.sandbox.taxcore.online/"data-taxcore-input-id="invoiceRequest"data-taxcore-output-id="results"data-taxcore-invoice-request=""data-taxcore-debug="true"data-taxcore-signed-invoice-response="">Sign Invoice</button>
    <labelfor="results">Received Signed Invoice:</label>    
    <textareareadonly id="results"cols="100"rows="30"></textarea>
    <!--TAXCORE.JS-->
    <scriptsrc=" https://vsdc.sandbox.taxcore.online/onlinepos/v1/taxcore.min.js"></script>    
    <!--Custom script at Online POS-->
    <script>document.getElementById("invoiceRequest").innerHTML=JSON.stringify(CreateExampleInvoiceRequest(), undefined,4);        

document.getElementById(
"taxcore_sign_element").dataset.taxcoreInvoiceRequest=JSON.stringify(CreateExampleInvoiceRequest());//Listen to messages from TaxCore window.onmessage= function(e) {
console.log(e.data);
}
functionCreateExampleInvoiceRequest() {varinvoiceRequest={"DateAndTimeOfIssue":"2017-08-31T13:28:02.433Z","Cashier":"John","BD":null,"BuyerCostCenterId":null,"IT":"Normal","TT":"Sale","PaymentType":"Card","InvoiceNumber":"31082017-2","ReferentDocumentNumber":null,"PAC":null,"Options": {"OmitTextualRepresentation":0,"OmitQRCodeGen":0},"Items": [
{
"GTIN":null,"Name":"Book","Quantity":1,"Labels": ["A"],"TotalAmount":50}
]
};
returninvoiceRequest;
}
</script> </body> </html>

在国内是不存在网络问题的,所以官方提供了HTML在线POS的示例代码。那么我认为有必要提一下为什么最终我选择了用Winform来做这个客户端程序。第一我有多年的CS程序开发经验,在这里3G和4G都满如蜗牛一样的网络条件下我首先想到的是自动实现离线和在线模式切换,先把交易数据存在本地的Sqlite中,一旦网络可用才上传到服务器,而且针对离线交易不能打印在线的财政发票的情况下TaxCore提供了半连接模式可以使用智能卡存储交易数据,一旦网络恢复则从卡里读取数据再上传,提供了一种本地审核的方式,就是离线可以打印财政发票。由于我对WPF等技术不太熟练,综合考虑于是就使用了WinForm来开发客户端程序。下面贴一点WinForm发起发票请求的代码:

 /// <summary>
 ///发起联网发票请求/// </summary>
 /// <param name="bill">订单</param>
 public voidRequestNormalInvoice(OrderTableEntity bill)
{
#region 构建请求jsonTaxCoreBobyRequest request= newTaxCoreBobyRequest();
request.Cashier
=bill.casher;if (CurrentMemberObj != null)
request.BD
= string.IsNullOrEmpty(CurrentMemberObj.Buyer_Tin) == true ? null: CurrentMemberObj.Buyer_Tin;elserequest.BD= null;
request.BuyerCostCenterId
= null;
request.IT
= "Normal";
request.TT
= "Sale";
request.InvoiceNumber
=bill.orderNo;
request.ReferentDocumentNumber
= null;
request.PAC
= $"{ConfigObj.PAC}";var s1 = new BaseRepository<BillPayDetailEntityTable>();var s2 = new BaseRepository<BillConsumeDetailEntityTable>();var s3 = new BaseRepository<Payment_method_tableEntity>();var list1 = s1.GetList(p => p.order_No ==bill.orderNo);var list2 = s2.GetList(p => p.order_No ==bill.orderNo);
List
<GoodsItem> items = new List<GoodsItem>();var goodsSvr = new BaseRepository<GoodsTableEntity>();#region 商品明细 foreach (var item inlist2)
{
var goods = goodsSvr.GetFirst(g => g.goodsNo ==item.goods_No);
items.Add(
newGoodsItem
{
GTIN
= null,
Name
=item.goods_Name,
Quantity
=item.qty,
UnitPrice
= item.sale_price -item.discountAmount,
TotalAmount
= item.qty * (item.sale_price -item.discountAmount),
Labels
= new string[]
{
goods
== null ?ConfigObj.MISCELLANEOUS_Rate_Label : goods.taxLabels
}
});
}
#endregionrequest.Items=items;var pInfo = s3.GetList(p => p.pm_Id ==list1.FirstOrDefault().payment_method).FirstOrDefault();if (pInfo.TaxcoreName !=pInfo.pm_Name)
request.PaymentType
=pInfo.TaxcoreName;elserequest.PaymentType=pInfo.pm_Name;#endregion string invoiceRequest =JsonConvert.SerializeObject(request);var httpContent = new StringContent(invoiceRequest, Encoding.UTF8, "application/json");
HttpClient client;
WebRequestHandler handler;
GetClientAndHandler(
out handler, outclient);var response = client.PostAsync($"api/Sign/SignInvoice", httpContent).Result;if (response.StatusCode ==HttpStatusCode.OK)
{
var svr = new BaseRepository<OrderTableEntity>();bool b = invoiceRequest.Contains("'");if(b)
invoiceRequest
= invoiceRequest.Replace("'", " ");string cmd = $"update orders set fiscalInvoiceRequest='{invoiceRequest}' where orderNo='{bill.orderNo}'";
svr.Context.Ado.ExecuteCommand(cmd);
var jsonString =response.Content.ReadAsStringAsync();
jsonString.Wait();
var invoiceResponse =jsonString.Result;var responseObj = JsonConvert.DeserializeObject<TaxCoreResponse>(invoiceResponse);
orderTableEntity
=bill;
PrintNormalInvoice(responseObj);
responseObj.VerificationQRCode
= "";
responseObj.ID
= "";
responseObj.S
= "";
responseObj.Journal
= "";string cmd_sql = $"update orders set UploadDateTime=null,fiscalInvoiceResponse='{JsonConvert.SerializeObject(responseObj)}' where orderNo='{bill.orderNo}'";
svr.Context.Ado.ExecuteCommand(cmd_sql);
}
else{
MessageTip.ShowError($
"Failed to request invoice:[{response.StatusCode}]");throw new Exception($"Failed to request invoice:[{response.StatusCode}]");
}
}
/// <summary> ///请求地址赋值/// </summary> /// <param name="handler"></param> /// <param name="client"></param> public void GetClientAndHandler(out WebRequestHandler handler, outHttpClient client)
{
handler
=CreateWebRequestHandler();
client
= newHttpClient(handler);
client.BaseAddress
= new Uri($"{ConfigObj.RequstBaseURL}");
client.DefaultRequestHeaders.Accept.Clear();
}
/// <summary> ///请求加入证书/// </summary> /// <returns></returns> privateWebRequestHandler CreateWebRequestHandler()
{
var handler = newWebRequestHandler();var cert =GetClientCertificate();
handler.ClientCertificateOptions
=ClientCertificateOption.Manual;
handler.ClientCertificates.Add(cert);
returnhandler;
}
/// <summary> ///读取证书/// </summary> /// <returns></returns> privateX509Certificate2 GetClientCertificate()
{
X509Store store
= newX509Store(StoreName.My, StoreLocation.CurrentUser);//store.Open(OpenFlags.ReadWrite); string certName = $"{ConfigObj.InvoiceCertificateName}";
store.Open(OpenFlags.OpenExistingOnly
|OpenFlags.ReadOnly);var certificates =store.Certificates;foreach (X509Certificate2 cert incertificates)
{
if(cert.Subject.Contains(certName))
{
returncert;
}
}
return null;
}

关于打印的代码就有点冗长了,请无视我的复制和粘帖运用得如此炉火纯青。

/// <summary>
///发票绘制/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PdInvoice_PrintPage(objectsender, PrintPageEventArgs e)
{
string[] journal =currentResponse.Journal.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);string pos_time = journal[9].Replace("POS Time:", "");if (!string.IsNullOrEmpty(orderTableEntity.member_No))
pos_time
= journal[10].Replace("POS Time:", "");var s1 = new BaseRepository<BillConsumeDetailEntityTable>();var s2 = new BaseRepository<BillPayDetailEntityTable>();var list1 = s1.GetList(o => o.order_No ==orderTableEntity.orderNo);var list2 = s2.GetList(o => o.order_No ==orderTableEntity.orderNo);int right_space = 32;
e.Graphics.Clear(Color.White);
Rectangle rect
= new Rectangle(0, 0, 300, 360);#region StringFormatStringFormat sf_center= newStringFormat();
sf_center.Alignment
=StringAlignment.Center;
sf_center.LineAlignment
=StringAlignment.Center;
StringFormat sf_left
= newStringFormat();
sf_left.Alignment
=StringAlignment.Near;
sf_left.LineAlignment
=StringAlignment.Center;
StringFormat sf_right
= newStringFormat();
sf_right.Alignment
=StringAlignment.Far;
sf_right.LineAlignment
=StringAlignment.Center;#endregionFont tipsFont= new Font("微软雅黑", 9F, FontStyle.Bold);
Font txtFont
= new Font("微软雅黑", 9F);
e.Graphics.DrawString($
"{merchantStoreEntity.name}", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_center);
rect.Y
+= 20;
e.Graphics.DrawString($
"===========================================", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 20;
e.Graphics.DrawString($
"PH#:{merchantStoreEntity.phone} MOB#:{merchantStoreEntity.mobile}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_center);if (!string.IsNullOrEmpty(merchantStoreEntity.store_advertising))
{
rect.Y
+= 20;
e.Graphics.DrawString($
"{merchantStoreEntity.store_advertising}", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_center);
}
rect.Y
+= 20;
e.Graphics.DrawString($
"========= FISCAL INVOICE ===============", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 20;
e.Graphics.DrawString($
"TIN:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString(currentResponse.TIN, txtFont, Brushes.Black,
new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"Company:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{currentResponse.BusinessName}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"Store:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{merchantStoreEntity.name}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"Address:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{currentResponse.Address}", txtFont, Brushes.Black, new Rectangle(42, rect.Y, rect.Width - 76, 40), sf_right);
rect.Y
+= 40;
e.Graphics.DrawString($
"District:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{currentResponse.District}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"Cashier TIN:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{CashierFlowContext.Instance.CurrentEmployee.employeeName}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"POS Number:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{orderTableEntity.orderNo}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"POS Time:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString(pos_time.Trim(), txtFont, Brushes.Black,
new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);if (!string.IsNullOrEmpty(orderTableEntity.member_No))
{
rect.Y
+= 20;
e.Graphics.DrawString($
"Buyer TIN:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{orderTableEntity.member_No}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
}
rect.Y
+= 20;
e.Graphics.DrawString($
"-----------------NORMAL SALE--------------------", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 20;
e.Graphics.DrawString($
"Items", txtFont, Brushes.Black, new Rectangle(4, rect.Y, rect.Width, 20), sf_center);
rect.Y
+= 20;
e.Graphics.DrawString($
"===========================================", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 18;
e.Graphics.DrawString($
"Name Price Qty. Total", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 16;var goodsSvr = new BaseRepository<GoodsTableEntity>();foreach (var detailRow inlist1)
{
var goods = goodsSvr.GetFirst(g => g.goodsNo ==detailRow.goods_No);string label = goods == null ?ConfigObj.MISCELLANEOUS_Rate_Label : goods.taxLabels;
e.Graphics.DrawString($
"{detailRow.goods_Name} ({label})", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{detailRow.sale_price.ToString("C2").Substring(1)}", txtFont, Brushes.Black, new Rectangle(rect.Width / 2 - 86, rect.Y + 16, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{detailRow.qty}", txtFont, Brushes.Black, new Rectangle(rect.Width / 2 - 8, rect.Y + 16, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{detailRow.amount.ToString("C2").Substring(1)}", txtFont, Brushes.Black, new Rectangle(0, rect.Y + 16, rect.Width - right_space - 8, 20), sf_right);
rect.Y
+= 34;
}
e.Graphics.DrawString($
"Total Purchase:", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{orderTableEntity.payAmount.ToString("C2").Substring(1)}", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;var service = new BaseRepository<Payment_method_tableEntity>();var p = service.GetFirst(pm => pm.pm_Id ==list2.FirstOrDefault().payment_method);
e.Graphics.DrawString($
"Payment Method:", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);if (p.TaxcoreName !=p.pm_Name)
e.Graphics.DrawString(p.TaxcoreName, tipsFont, Brushes.Black,
new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);elsee.Graphics.DrawString($"{p.pm_Name}", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"===========================================", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 18;
e.Graphics.DrawString($
"Label Name Rate", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString(
"Tax", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;foreach (var rate incurrentResponse.TaxItems)
{
e.Graphics.DrawString($
"{rate.Label} {rate.CategoryName} {rate.Rate}%", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{rate.Amount.ToString("C2").Substring(1)}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
}
e.Graphics.DrawString($
"--------------------------------------------------", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 20;
e.Graphics.DrawString($
"Total Tax:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{currentResponse.TaxItems.Sum(t => t.Amount).ToString("C2").Substring(1)}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"===========================================", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 20;
e.Graphics.DrawString($
"SDC Time:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);string dt = Convert.ToDateTime(currentResponse.DT).ToString("dd/MM/yyyy HH:mm:ss");if (ConfigObj.RequstBaseURL.Contains("sandbox"))
{
dt
= Convert.ToDateTime(currentResponse.DT.AddHours(-12)).ToString("dd/MM/yyyy HH:mm:ss");//.ToString("dd/MM/yyyy HH:mm:ss"); }
e.Graphics.DrawString($
"{dt}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"SDC Invoice No:", new Font("微软雅黑", 8.2F), Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_left);
e.Graphics.DrawString($
"{currentResponse.IN}", new Font("微软雅黑", 8.2F), Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"Invoice Counter:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_left);
e.Graphics.DrawString($
"{currentResponse.IC}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"===========================================", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 20;byte[] bytes =Convert.FromBase64String(currentResponse.VerificationQRCode);
MemoryStream memStream
= newMemoryStream(bytes);
Image mImage
=Image.FromStream(memStream);
e.Graphics.DrawImage(mImage,
new Rectangle(8, rect.Y, 266, 266));
rect.Y
+= 266;
e.Graphics.DrawString($
"====== END OF FISCAL INVOICE ==========", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 20;
e.Graphics.DrawString($
"Subtotal ({list1.Count()} items)", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{list1.Sum(o => o.amount).ToString("C2").Substring(1)}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);if (orderTableEntity.payMethodName.Contains("->("))
{
rect.Y
+= 20;
e.Graphics.DrawString($
"===========Aggregated payment============", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
rect.Y
+= 20;
e.Graphics.DrawString($
"Payment method:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{orderTableEntity.payMethodName}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"Payment Amount:", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{orderTableEntity.payAmount.ToString("C2").Substring(1)}", txtFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
}
if (p.is_cash.ToUpper().Equals("Y") && (list2.Any(o => o.exchangeAmount > 0)))
{
rect.Y
+= 20;
e.Graphics.DrawString($
"Collection:", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{(orderTableEntity.payAmount + orderTableEntity.payBackAmount).ToString("C2").Substring(1)}", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
rect.Y
+= 20;
e.Graphics.DrawString($
"Change:", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_left);
e.Graphics.DrawString($
"{orderTableEntity.payBackAmount.ToString("C2").Substring(1)}", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width - right_space, 20), sf_right);
}
if (!string.IsNullOrEmpty(merchantStoreEntity.store_advertising_footer))
{
rect.Y
+= 22;
e.Graphics.DrawString($
"{merchantStoreEntity.store_advertising_footer}", tipsFont, Brushes.Black, new Rectangle(0, rect.Y, rect.Width, 20), sf_center);
}

}

现有软件的一些截图

本篇是Mygin第五篇

目的

  • 实现路由分组

为什么要分组

分组控制(Group Control)是 Web 框架应该提供的基础功能之一,对同一模块功能的开发,应该有相同的前缀。或者对一部分第三方接口,统一需要加解密等功能。分组后很方便。例如:

  • 对于任务模块,统一前缀为/task
  • 除去/user/login接口,都需要鉴权
  • 以/openapi 开头的接口,需要对接第三方平台,需要三方平台鉴权

大多数分组都是统一前缀,正确的分组可以实现子分组,无限极往下分组,当然实际情况下也不会有太多层分组。每个分组有不同的中间件(middleware),分组与子分组就像洋葱一样,一层一层往内。要想往内,需要拨开最外层,也就是要执行外层的中间件才能往内。对于分组,也有后续扩展的好处,比如/task分组,现在要统一加上访问日志记录,有了分组后就可以在该分组上添加一个中间件就可以了。

分组嵌套

在实现分组嵌套之前,先看看gin中的分组嵌套是怎么实现的。gin中的分组中含有Engin的指针,其实很好理解,因为分组要有访问Router的能力。Engin中继承了RouterGroup,实例化Engin后就可以使用Group的功能。有了Engin的指针,整个框架的所有资源都是由Engine去统一调度的,因此通过Engine可以间接地拥有整个框架的功能。你中有我我中有你。

  • gin/routergroup.go
package mygin

import (
	"net/http"
	"path"
	"regexp"
)

// IRoutes 定义了路由组的接口
type IRoutes interface {
	BasePath() string
	GET(string, ...HandlerFunc) IRoutes
	POST(string, ...HandlerFunc) IRoutes
	DELETE(string, ...HandlerFunc) IRoutes
	PATCH(string, ...HandlerFunc) IRoutes
	PUT(string, ...HandlerFunc) IRoutes
	OPTIONS(string, ...HandlerFunc) IRoutes
	HEAD(string, ...HandlerFunc) IRoutes
	Match([]string, string, ...HandlerFunc) IRoutes
}

// anyMethods 包含所有 HTTP 方法的字符串表示
var anyMethods = []string{
	http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
	http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
	http.MethodTrace,
}

// RouterGroup 定义了路由组的结构体
type RouterGroup struct {
	Handlers HandlersChain // 路由组的中间件处理函数链
	basePath string        // 路由组的基础路径
	engine   *Engine       // 路由组所属的引擎
	root     bool          // 是否是根路由组
}

// Group 创建一个新的路由组
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
		Handlers: append(group.Handlers, handlers...),
		basePath: path.Join(group.basePath, relativePath),
		engine:   group.engine,
	}
}

// BasePath 返回路由组的基础路径
func (group *RouterGroup) BasePath() string {
	return group.basePath
}

// handle 处理路由,将路由信息添加到引擎中
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := path.Join(group.basePath, relativePath)
    //处理函数,Group组定义的函数先执行,自定义的函数后执行
	handlers = append(group.Handlers, handlers...)
	group.engine.addRoute(httpMethod, absolutePath, handlers)

	if group.root {
		return group.engine
	}
	return group
}

// Handle 校验 HTTP 方法的有效性,并处理路由
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
	// 检查 HTTP 方法的有效性
	if match := regexp.MustCompile("^[A-Z]+$").MatchString(httpMethod); !match {
		panic("http method " + httpMethod + " is not valid")
	}
	// 处理路由
	return group.handle(httpMethod, relativePath, handlers)
}

// GET 注册 GET 方法的路由
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

// POST 注册 POST 方法的路由
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPost, relativePath, handlers)
}

// DELETE 注册 DELETE 方法的路由
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodDelete, relativePath, handlers)
}

// PATCH 注册 PATCH 方法的路由
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPatch, relativePath, handlers)
}

// PUT 注册 PUT 方法的路由
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPut, relativePath, handlers)
}

// OPTIONS 注册 OPTIONS 方法的路由
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodOptions, relativePath, handlers)
}

// HEAD 注册 HEAD 方法的路由
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodHead, relativePath, handlers)
}

// Match 注册多个方法的路由
func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes {
	for _, method := range methods {
		group.handle(method, relativePath, handlers)
	}

	if group.root {
		return group.engine
	}
	return group
}

// Any 注册所有方法的路由
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
	for _, method := range anyMethods {
		group.handle(method, relativePath, handlers)
	}

	if group.root {
		return group.engine
	}
	return group
}

可以看到group.engine.addRoute方法,实际调用的仍然是mygin/router.go中的addRoute方法。当group.root为true,也就是为根时,相当于没有调用Group方法,返回group.engine,也就是相当于直接调用的engine,只有当调用Group方法后才会返回group。
接下来看engine的修改

  • mygin/engine.go
package mygin

import (
	"net/http"
)

// HandlerFunc 定义处理函数类型
type HandlerFunc func(*Context)

// HandlersChain 定义处理函数链类型
type HandlersChain []HandlerFunc

// Engine 定义引擎结构,包含路由器
type Engine struct {
	Router
	RouterGroup
}

// ServeHTTP 实现http.Handler接口的方法,用于处理HTTP请求
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 获取对应HTTP方法的路由树的根节点
	root := e.trees.get(r.Method)
	// 解析请求路径
	parts := root.parseFullPath(r.URL.Path)

	// 查找符合条件的节点
	searchNode := make([]*node, 0)
	root.search(parts, &searchNode)

	// 没有匹配到路由
	if len(searchNode) == 0 {
		w.Write([]byte("404 Not found!\n"))
		return
	}

	// 参数赋值
	params := make([]Param, 0)
	searchPath := root.parseFullPath(searchNode[0].fullPath)
	for i, sp := range searchPath {
		if sp[0] == ':' {
			params = append(params, Param{
				Key:   sp[1:],
				Value: parts[i],
			})
		}
	}

	// 获取处理函数链
	handlers := searchNode[0].handlers
	if handlers == nil {
		w.Write([]byte("404 Not found!\n"))
		return
	}

	// 执行处理函数链
	for _, handler := range handlers {
		handler(&Context{
			Request: r,
			Writer:  w,
			Params:  params,
		})
	}
}

// Default 返回一个默认的引擎实例
func Default() *Engine {
	engine := &Engine{
		Router: Router{
			trees: make(methodTrees, 0, 9),
		},
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
	}

	// Group 保存 engine 的指针
	engine.RouterGroup.engine = engine

	return engine
}

// Run 启动HTTP服务器的方法
func (e *Engine) Run(addr string) error {
	return http.ListenAndServe(addr, e)
}

最后来测试一下

  • main.go
package main

import (
	"gophp/mygin"
	"path"
)

func main() {
	// 创建一个默认的 mygin 实例
	r := mygin.Default()

	group := r.Group("/api", func(context *mygin.Context) {
		context.String("api Group ....\n")
	})

	group.GET("/hello/:name", func(context *mygin.Context) {
		name := context.Params.ByName("name")
		context.String(path.Join("hello ", name, "!"))
	})

	group.GET("/hello2/:name", func(context *mygin.Context) {
		name := context.Params.ByName("name")
		context.String(path.Join("hello2 ", name, "!\n"))
	})

	// 启动服务器并监听端口
	r.Run(":8088")
}

curl请求测试

 curl -i http://localhost:8088/api/hello/scott
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Wed, 24 Jan 2024 05:16:45 GMT
Content-Length: 29

api Group ....
hello /scott/!
~ curl -i http://localhost:8088/api/hello2/scott
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Wed, 24 Jan 2024 05:16:54 GMT
Content-Length: 31

api Group ....
hello2 /scott/!

可以看到两个路由都安装预定的返回了,即先返回Group定义的,再返回路由定义的。

1. 前言

大家好,我叫颜国进,现为英特尔边缘计算创新大使、百度飞桨开发者专家。回溯至2021年12月,那时的我,身为机械专业研一新生,仅在C和Python编程语言上有些许基础,对于深度学习的殿堂还只敢在门口窥探,对于OpenCV图像处理以及模型部署等技术更是所知甚少。然而,通过不懈的努力和对知识的执着追求,如今我已经能够娴熟地运用C++、C#等编程语言进行项目开发,并且能够使用Python和各类深度学习框架来训练自己的模型。值得一提的是,我已经发布并运营了一项名为OpenVINO C# API的开源项目,为在C#平台开发深度学习和计算机视觉应用的开发者提供更便捷的工具。围绕该项目,产出了多篇富有深度的技术文章,分享了我在使用OpenVINO部署深度学习模型过程中的洞见和体会。

在深入参与开源项目的过程中,我也广泛参考了网上的一些成功项目经验,得到了无数宝贵的经验和技能提升,这使得我的技术层次上升到了全新的高度。经过两年的探索与实践,技术能力得到了显著的提高,而更重要的是,我的努力得到了越来越多人的认可和支持,从而扩大了我在技术社区的影响力。时值我开展开源项目的两周年,我有幸应张晶和李翊玮老师的邀请,以及武卓博士的悉心指导,有机会整理与分享我从一个技术新手逐步成长到拥有自己开源项目的经验与感想。我希望我的分享能为刚刚踏入开源领域的新手们提供一些有价值的参考,帮助他们更迅速地融入这个富有活力和创新精神的开源社区。
下面,我将从四个层面并结合具体的问题分别向大家:

2. 为什么要做开源

问题一、为什么要给开源项目和社区做贡献?

首先,我们来探讨一下为什么应该为开源项目和社区做出贡献。对于那些还在犹豫不决是否投身于开源项目的小伙伴们,我希望能够提供一个有力的依据。参与开源项目不仅有助于个人的技能提升和职业发展,更能推动整个技术社区的进步。通过你的贡献,我们可以共同创造一个更加开放、协作和创新的技术生态,让每个人都能从中受益。

  • 参与开源项目和社区对个人具有多方面的积极影响。


    • 技术提升
      ——通过参与开源项目,个人能够接触和学习到更多的技术工具和前沿技术,推动自身技术的不断提升。并且开源项目可以获取全部源码,这对你深入了解一项技术实现以及应用,会有很大的帮助。

    • 提升个人的声誉和影响力
      ——通过专注于某一领域并在其中深耕,你可能成为该领域的专家,吸引其他技术专家的关注和求助。这些互动和认可不仅有助于建立个人的专业声誉,还可能带来一些荣誉称号,为未来的发展提供有力的支持。

    • 塑造个人的未来发展轨迹
      ——开源的自由性和包容性使得个人能够自主选择感兴趣的领域进行探索和发展。你可以将开源项目中的经验和技能应用到自己喜欢的领域中,或者通过开源项目发展新的兴趣和专业方向。这将为个人的职业发展带来更多的可能性和机遇。总之,参与开源项目和社区对个人技术提升、声誉建立以及未来发展都具有积极的影响。这是一个充满挑战和机遇的舞台,值得每个有志于技术领域的人积极参与和贡献。

  • 开源还对当前行业以及企业发展带来了更多的红利。


    • 开源已经成为软件行业的基础
      ——开源促进了技术的快速发展和创新,第一点,由于代码是公开的,开发者可以基于现有的项目进行创新,而不需要从零开始,这样的协作和共享精神加速了新技术的诞生和成熟。第二点,开源软件通常是免费提供的,这降低了企业和个人使用和实施新技术的成本,企业可以使用开源软件来构建复杂的系统,而无需支付昂贵的授权费用。第三点,开源软件赋予用户更大的自由度,用户可以自由地修改和改进软件以满足自己的需求,这种灵活性对于需要定制解决方案的企业和开发者来说是非常重要的。第四点,开源项目通常有一个活跃的社区,社区成员可以相互支持,解决问题,分享经验。这种广泛的协作和支持网络是传统闭源软件难以比拟的。第五点,开源项目可以非常迅速地迭代和改进,社区贡献者可以快速响应问题,提交补丁,添加新功能,从而保持软件的活力。基于此,可以看出开源软件已经深刻地影响了软件产业的各个方面,并且在很多情况下成为了技术创新和应用的基石。
      image

    • 开源已经成为当前最前沿技术的发展的强大动力
      ——开源模式鼓励全球范围内的协作和知识共享,由于开源项目可以被任何人审查、使用、修改和改进,这样的透明度和可接触性降低了进入门槛,极大地促进了创新的速度和质量。此外开源社区的参与者可以迅速识别和修复软件中的错误,增加新的功能,并持续改进产品,这种集体智慧的应用使得技术能够快速迭代和演进,经常超出单一组织的开发速度。开源项目往往吸引来自不同组织和行业的贡献者。这种多元化的视角和专业知识结合在一起,可以解决复杂的问题,推动前沿技术的发展。开源项目还是人才发现和发展的平台,许多开发者通过参与开源项目获得实战经验,进而成为行业内的专家。最后开源项目提供了高质量的软件基础设施,许多公司和组织可以直接使用或构建自己的产品,比如Linux操作系统、Apache Web服务器、MySQL数据库、TensorFlow机器学习框架、OpwenVINO模型部署框架等,都是开源项目,它们为最前沿的技术发展提供了坚实的基础。开源项目围绕它们构建了强大的生态系统,这个生态系统是持续创新的土壤,帮助技术不断进步。
      image

废话了这么多,但也很难完全表述给开源项目和社区做贡献的好处,下面我将从个人经历简单讲述一下做开源项目这两年,到底给我带来了啥?

  • 技术方面
    :刚开始做开源项目时,我的状态就是一个非计算机专业、没有编程经验和算法基础的新手小白,蜕变成为一个可以实现在C/C++、C#和Python环境下使用多种部署工具套件落地AI设备全流程开发,并且在稳定运行了多个小型开源项目。在这过程中,我学习了C++、C#以及Python编程语言,这样我就可以能够看懂开源新项目的源码;我做的主要领域时深度学习模型部署,因此我自学了OpenVINO、TensorRT、ONNX runtime 等模型部署工具,实现在各种平台下部署深度学习模型。并且由于所做工作为开源项目,为了遵循开源社区规范,自己也养成了良好的编程习惯以及代码规范,学会了开源项目从创建、提交、完善、维护、更新以及发布等全套流程。
    image

  • 开源贡献
    :结合自己接触的一些开源项目,自己选者了一个方向去做,主要是在C#平台部署深度学习模型。基于OpenVINO模型部署工具创建了OpenVINO C# API项目,并结合该项目,开发了当前流行的模型部署案例。截止到目前,自己已经在开源平台GitHub平台贡献了100,000多行代码,推出了10多个开源项目。除此以外,自己还结合所做的开源项目,在多个平台与开源社区发布了了多篇技术文章,获得了上千人的阅读,并且在CSDN平台打造自己的博客平台,截止目前已经发表了42篇文章,获得了80000+的阅读量以及1400+的粉丝关注,在分享自己案例指导指导其他开发者的同时,也收获了更多人的关注,提升了自己的影响力。同时由于自己专注于英特尔以及百度飞桨的开源项目工作,因此获得了英特尔边缘计算创新大使、百度飞桨开发者专家等荣誉称号。
    image

上面只是简单总结了自己这两年以来做开源项目所获得的东西,可以看出,给开源项目和社区做贡献,在成就开源社区的同时,更大的就是成就了自己。

3. 如何为开源做贡献

上面我们讲了这么多给开源项目和社区做贡献的好处,可能有一些小伙伴就开始心动了,下面我们就展开讲述一下有哪些途径可以为开源做贡献,具体是如何做的。

问题二:为开源项目做贡献--如何提Issue?

首先向大家介绍一下第一种方式,也是入门级别相对简单也是比较容易实现的,就是给开源项目提交Issue。开源项目在发布时尽管发布者或者团队已经做了很详尽的测试,但由于使用对象以及使用环境等因素的影响,使用者难免会出现相关问题以及新的使用需求,因此我们提交Issue可以包括以下内容:

  • Bug报告
    :你发现了代码中的错误或异常行为。
  • 功能请求
    :你希望项目中添加一个新功能或改进现有功能。
  • 问题咨询
    :你在使用或配置项目时遇到了困难。
  • 文档改进
    :你认为项目的文档需要更清晰或更完整。

当然,这是是我列出来的常见内容,具体还是看使用者需求,因此提交Issue一定是该开源项目的使用者,再使用时碰到难以解决的问题时我们就可以通过提交Issue方式解决,这样不仅可以帮助自己解决问题,还可以促进开源项目的改进与发展,属于两全其美的事情。

再确定了你的问题以及你要提交Issue时,你需要先做一步问题检查,检查一下是否有小伙伴提交过相同的Issue,这样可以避免提交重复的问题,给开源项目发布人员带来不必要的负担,也是对做开源工作对其他人员的最起码的尊重。如果你发现这是一个新的问题,之前并没有人提过,那么好,你就可以开始提交你的Issue了,主要包括以下几个步骤:

(1)创建一个新的Issue

此处以OpenVINO厂库为例,创建一个新的Issue。

image

OpenVINO官方提供了不同的Issue类型选择,此处可以根据你所需要的问题类型进行选择。

image

(2)编写Issue标题:

使用简洁明了的标题来描述你的问题或请求,好的标题可以快速地让维护者了解Issue的主要内容。

(3)详细描述问题:

官方人员在解决你所提出来的问题时,会首先复现你所出现的问题,因此在提交Issue时,一定要详细描述你所提交的问题复现步骤以及结果输出情况和日志,总的来说可以包括以下几个部分:

  • 上下文:描述你在什么情况下遇到的问题。
  • 重现步骤:如果是bug报告,详细列出如何重现这个问题。
  • 期望结果与实际结果:说明你的期望是什么,以及实际发生了什么。
  • 相关信息:如操作系统、软件版本、依赖库版本等。
  • 附加信息:
    • 日志和错误消息:如果有相关的日志或错误消息,请附加或链接到它们。
    • 屏幕截图和视频:如果它们有助于解释问题,可以附加在后面。

image

最后就是要使用Markdown格式来组织你的描述,使其易于阅读。并始终保持礼貌和尊重,因为开源维护者是在免费为你提供帮助。

(4)提交并等待回复

最后在详细描述完问题内容后就可以提交问题了,提交后就可以等待光放人员的回复,如光官方人员在复现时或者解决时有什么问题,也会向你进行反馈,因此可以时刻关注问题的动态。最后在问题解决后一定要礼貌感谢一下官方人员。

** 问题三:为开源项目做贡献--如何提PR?**

上面我们已经介绍了提交Issue,这对于刚入门的开发者来说是十分容易的,下面我们将介绍一下进阶内容,给开源项目提交PR。提交PR应该属于进阶版内容,因为他是从技术要求、编程能力以及对源码的掌握程度都会有很高的要求,所以这一块内容适合对项目十分熟悉的小伙伴。

但如果小伙伴们到了这种程度或者是在使用中对项目源码做了修改使其变得更好,就可以开始向项目提交PR了,不过在提交前要先注意以下几点:

  • 第一,确定一下该项目支不支持提交PR,此处主要是通过该项目厂库查看是否PR板块是否开放,以及在项目README中,作者是否表达了欢迎广大开发者对该项目进行改进的意愿。
  • 第二,查看一下项目官方对提交PR的要求,这个要求一般会在项目厂库根目录下的CONTRIBUTING.md文件中,该文件中会规定了具体的提交PR的要求,包括提交PR的规范、代码风格以及编码格式要求和开源许可等内容。

在确定了以上问题后,就可以开始提交你的PR了,这里有两种方式可以提交PR,一种是在线直接提交,这种方式适合修改内容较少,不涉及源码修改等;第二种是本地修改后提交,这种方式适合有较大的改动,涉及到源码修改等。

方式一:在线直接提交

(1)选择要更新的文件

通过在 GitHub 中单击它来选择更新,以README.md文件为例:
image

(2)点击“编辑此文件”

该按钮位于右上角。
image

如果你没有Fork该项目,需要先Fork该项目。
image

(3)修改文件内容


此处增加两个

前言

近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。

本项目为前后端分离开发,后端基于
Java21

SpringBoot3
开发,后端使用
Spring Security

JWT

Spring Data JPA
等技术栈,前端提供了
vue

angular

react

uniapp

微信小程序
等多种脚手架工程。

本文主要介绍在
SpringBoot3
项目中如何整合
Redis
,JDK版本是
Java21

项目地址:
https://gitee.com/breezefaith/fast-alden

相关技术简介

Redis

Redis
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

image

和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部分场合可以对关系数据库起到很好的补充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,使用很方便。
Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

官网:
http://redis.io/

Redis 常用数据类型使用场景:

  • String,存短信验证码、缓存、计数器、分布式session
  • List,发布订阅等
  • Set,共同好友、点赞或点踩等
  • Hash,存储对象
  • Zset,排行榜
  • HyperLogLog,在线用户数、统计访问量等
  • GeoHash,同城的人、同城的店等
  • BitMap,签到打卡、活跃用户等

实现步骤

引入maven依赖


pom.xml
中添加
spring-boot-starter-data-redis
以及相关依赖。

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <!-- 解决在实体类使用java.time包下的LocalDateTime、LocalDate等类时序列化/反序列化报错的问题 -->
  <dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.13.0</version>
  </dependency>
</dependencies>

项目中引入
spring-boot-starter-data-redis
后默认使用
Lettuce
作为
Redis
客户端库。与老牌的
Jedis
客户端相比,
Lettuce
功能更加强大,不仅解决了线程安全的问题,还支持异步和响应式编程,支持集群,Sentinel,管道和编码器等等功能。

如果想使用
Jedis
,还需要引入
Jedis
相关依赖。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

修改配置文件

修改SpringBoot项目配置文件,本项目使用的是application.yml文件。

spring:
  data:
    redis:
      host: localhost     # Redis服务器地址
      port: 6379          # Redis服务器连接端口
      password: 123456    # Redis服务器连接密码(默认为空)
      database: 0         # Redis数据库索引(默认为0)
      timeout: 60s        # 连接空闲超过N(s秒、ms毫秒,不加单位时使用毫秒)后关闭,0为禁用,这里配置值和tcp-keepalive值一致
      # Lettuce连接池配置
      lettuce:
        pool:
          max-active: 10  # 允许最大连接数,默认8(负值表示没有限制),推荐值:大于cpu * 2,通常为(cpu * 2) + 2
          max-idle: 8     # 最大空闲连接数,默认8,推荐值:cpu * 2
          min-idle: 0     # 最小空闲连接数,默认0
          max-wait: 5s    # 连接用完时,新的请求等待时间(s秒、ms毫秒),超过该时间抛出异常,默认-1(负值表示没有限制)

定义Redis配置类

在Redis配置类中,我们声明了一个自定义的
RedisTemplate<String, Object>
和一个自定义的Redis序列化器
RedisSerializer<Object>
,不声明也可以使用
Spring Boot
提供的默认的Bean。


/**
 * Redis相关Bean配置
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisSerializer<Object> serializer = redisSerializer();
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisSerializer<Object> redisSerializer() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //必须设置,否则无法将JSON转化为对象,会转化成Map类型
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);

        // 自定义ObjectMapper的时间处理模块
        JavaTimeModule javaTimeModule = new JavaTimeModule();

        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));

        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));

        objectMapper.registerModule(javaTimeModule);

        // 禁用将日期序列化为时间戳的行为
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        //创建JSON序列化器
        return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
    }
}

上述代码中针对
java.time
包下的
LocalDate

LocalDateTime

LocalTime
等类做了兼容,如果要缓存的实体数据中使用了
LocalDate

LocalDateTime

LocalTime
但没有自定义
ObjectMapper
的时间处理模块,可能会遇到如下报错。

2024-01-11T21:33:25.233+08:00 ERROR 13212 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: java.util.ArrayList[0]->com.fast.alden.data.model.SysApiResource["createdTime"])
	at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.serialize(Jackson2JsonRedisSerializer.java:157) ~[spring-data-redis-3.2.0.jar:3.2.0]
	at org.springframework.data.redis.core.AbstractOperations.rawValue(AbstractOperations.java:128) ~[spring-data-redis-3.2.0.jar:3.2.0]
	at org.springframework.data.redis.core.DefaultValueOperations.set(DefaultValueOperations.java:236) ~[spring-data-redis-3.2.0.jar:3.2.0]

image.png

这是因为Jackson库在默认情况下不支持
Java8

java.time
包下的
LocalDate

LocalDateTime

LocalTime
等类型的序列化和反序列化。错误堆栈中也给出了解决方案,添加
com.fasterxml.jackson.datatype:jackson-datatype-jsr310
依赖,但光添加依赖是不够的,还我们需要像上述代码一样自定义序列化和反序列化的行为。

定义Redis服务类,封装Redis常用操作

进行到此处时,其实我们已经可以通过获取
RedisTemplate<String, Object>
这个Bean来操作
Redis
了,但为了使用方便,我们定义了一个
RedisService
执行常用的
Redis
相关操作,实际上就是对
RedisTemplate<String, Object>
的进一步封装。

RedisService
接口定义如下。

/**
 * Redis操作服务类
 */
public interface RedisService {

    /**
     * 保存属性
     *
     * @param time 超时时间(秒)
     */
    void set(String key, Object value, long time);

    /**
     * 保存属性
     */
    void set(String key, Object value);

    /**
     * 获取属性
     */
    Object get(String key);

    /**
     * 删除属性
     */
    Boolean del(String key);

    /**
     * 批量删除属性
     */
    Long del(List<String> keys);

    /**
     * 设置过期时间
     */
    Boolean expire(String key, long time);

    /**
     * 获取过期时间
     */
    Long getExpire(String key);

    /**
     * 判断是否有该属性
     */
    Boolean hasKey(String key);

    /**
     * 按delta递增
     */
    Long incr(String key, long delta);

    /**
     * 按delta递减
     */
    Long decr(String key, long delta);

    /**
     * 获取Hash结构中的属性
     */
    Object hGet(String key, String hashKey);

    /**
     * 向Hash结构中放入一个属性
     */
    Boolean hSet(String key, String hashKey, Object value, long time);

    /**
     * 向Hash结构中放入一个属性
     */
    void hSet(String key, String hashKey, Object value);

    /**
     * 直接获取整个Hash结构
     */
    Map<Object, Object> hGetAll(String key);

    /**
     * 直接设置整个Hash结构
     */
    Boolean hSetAll(String key, Map<String, Object> map, long time);

    /**
     * 直接设置整个Hash结构
     */
    void hSetAll(String key, Map<String, ?> map);

    /**
     * 删除Hash结构中的属性
     */
    void hDel(String key, Object... hashKey);

    /**
     * 判断Hash结构中是否有该属性
     */
    Boolean hHasKey(String key, String hashKey);

    /**
     * Hash结构中属性递增
     */
    Long hIncr(String key, String hashKey, Long delta);

    /**
     * Hash结构中属性递减
     */
    Long hDecr(String key, String hashKey, Long delta);

    /**
     * 获取Set结构
     */
    Set<Object> sMembers(String key);

    /**
     * 向Set结构中添加属性
     */
    Long sAdd(String key, Object... values);

    /**
     * 向Set结构中添加属性
     */
    Long sAdd(String key, long time, Object... values);

    /**
     * 是否为Set中的属性
     */
    Boolean sIsMember(String key, Object value);

    /**
     * 获取Set结构的长度
     */
    Long sSize(String key);

    /**
     * 删除Set结构中的属性
     */
    Long sRemove(String key, Object... values);

    /**
     * 获取List结构中的属性
     */
    List<Object> lRange(String key, long start, long end);

    /**
     * 获取List结构的长度
     */
    Long lSize(String key);

    /**
     * 根据索引获取List中的属性
     */
    Object lIndex(String key, long index);

    /**
     * 向List结构中添加属性
     */
    Long lPush(String key, Object value);

    /**
     * 向List结构中添加属性
     */
    Long lPush(String key, Object value, long time);

    /**
     * 向List结构中批量添加属性
     */
    Long lPushAll(String key, Object... values);

    /**
     * 向List结构中批量添加属性
     */
    Long lPushAll(String key, Long time, Object... values);

    /**
     * 从List结构中移除属性
     */
    Long lRemove(String key, long count, Object value);
}

RedisService
实现类定义如下。

/**
 * Redis操作实现类
 */
@Service
public class RedisServiceImpl implements RedisService {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public void set(String key, Object value, long time) {
        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    }

    @Override
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    @Override
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    @Override
    public Boolean del(String key) {
        return redisTemplate.delete(key);
    }

    @Override
    public Long del(List<String> keys) {
        return redisTemplate.delete(keys);
    }

    @Override
    public Boolean expire(String key, long time) {
        return redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }

    @Override
    public Long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    @Override
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    @Override
    public Long incr(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    @Override
    public Long decr(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    @Override
    public Object hGet(String key, String hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    @Override
    public Boolean hSet(String key, String hashKey, Object value, long time) {
        redisTemplate.opsForHash().put(key, hashKey, value);
        return expire(key, time);
    }

    @Override
    public void hSet(String key, String hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
    }

    @Override
    public Map<Object, Object> hGetAll(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    @Override
    public Boolean hSetAll(String key, Map<String, Object> map, long time) {
        redisTemplate.opsForHash().putAll(key, map);
        return expire(key, time);
    }

    @Override
    public void hSetAll(String key, Map<String, ?> map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    @Override
    public void hDel(String key, Object... hashKey) {
        redisTemplate.opsForHash().delete(key, hashKey);
    }

    @Override
    public Boolean hHasKey(String key, String hashKey) {
        return redisTemplate.opsForHash().hasKey(key, hashKey);
    }

    @Override
    public Long hIncr(String key, String hashKey, Long delta) {
        return redisTemplate.opsForHash().increment(key, hashKey, delta);
    }

    @Override
    public Long hDecr(String key, String hashKey, Long delta) {
        return redisTemplate.opsForHash().increment(key, hashKey, -delta);
    }

    @Override
    public Set<Object> sMembers(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    @Override
    public Long sAdd(String key, Object... values) {
        return redisTemplate.opsForSet().add(key, values);
    }

    @Override
    public Long sAdd(String key, long time, Object... values) {
        Long count = redisTemplate.opsForSet().add(key, values);
        expire(key, time);
        return count;
    }

    @Override
    public Boolean sIsMember(String key, Object value) {
        return redisTemplate.opsForSet().isMember(key, value);
    }

    @Override
    public Long sSize(String key) {
        return redisTemplate.opsForSet().size(key);
    }

    @Override
    public Long sRemove(String key, Object... values) {
        return redisTemplate.opsForSet().remove(key, values);
    }

    @Override
    public List<Object> lRange(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    @Override
    public Long lSize(String key) {
        return redisTemplate.opsForList().size(key);
    }

    @Override
    public Object lIndex(String key, long index) {
        return redisTemplate.opsForList().index(key, index);
    }

    @Override
    public Long lPush(String key, Object value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }

    @Override
    public Long lPush(String key, Object value, long time) {
        Long index = redisTemplate.opsForList().rightPush(key, value);
        expire(key, time);
        return index;
    }

    @Override
    public Long lPushAll(String key, Object... values) {
        return redisTemplate.opsForList().rightPushAll(key, values);
    }

    @Override
    public Long lPushAll(String key, Long time, Object... values) {
        Long count = redisTemplate.opsForList().rightPushAll(key, values);
        expire(key, time);
        return count;
    }

    @Override
    public Long lRemove(String key, long count, Object value) {
        return redisTemplate.opsForList().remove(key, count, value);
    }
}

使用Redis服务类

下面以简单的登录和注销为例介绍Redis服务类的简单使用,登录成功时向Redis中插入了一条当前用户的记录,如果要查询系统当前在线用户可以从Redis中查询;注销时从Redis中找到在线用户记录并删除。

@Service
public class AuthServiceImpl extends AuthService {
    private final RedisService redisService;

    public AuthServiceImpl(RedisService redisService) {
        this.redisService = redisService;
    }

    public String login(LoginParam param) {
        // 根据登录参数查找用户,具体代码请自行实现
        SysUser user = new SysUser();
        // 根据用户信息生成token,具体代码请自行实现
        String token = "";
        // 在Redis中增加一条在线用户记录
        redisService.set("OnlineUser:" + user.getUsername() + ":" + token, user);

        return token;
    }

    public void logout() {
        // 获取当前用户,具体代码请自行实现
        SysUser user = new SysUser();
        // 获取当前用户token,具体代码请自行实现
        String token = "";
        // 清空登录信息,具体代码请自行实现

        // 删除Redis中当前用户记录
        redisService.del("OnlineUser:" + user.getUsername() + ":" + token);
    }
}

总结

本文简单介绍了一下
Redis

Redis
常见数据类型的使用场景,以及详细介绍了
SpringBoot3
整合
Redis
的详细过程,如有错误,还望批评指正。

在后续实践中我也是及时更新自己的学习心得和经验总结,希望与诸位看官一起进步。