2023年4月

前言

近日,苦于不知道该怎么提升自己了,在原来老大的建议下,决定去学习一些关于建模和软件设计领域的书籍,来解决解决自己“感觉不对,但是说不清楚为什么不对”以及“感觉这么搞就对了,但是不知道为什么这么去规划,这么去划分就对”

第一本看的是潘加宇老师的《软件方法(上)业务建模和需求》,本篇读后感不再对文里的概念和内容一一赘述,只说说个人提炼到的收获

业务系统是人脑系统的映射

这是一个很关键的概念,也是让我们认清业务系统的本质,所以我们讨论很多问题是,也就有了依据。

我们平时所看到的软件系统,都以业务系统为主,他们要么是人脑系统的替代,要么是现实流程的表示,明白了这一点本质以后,我们对系统的边界和其中逻辑的合理性,就可以有了一个标尺。而不是“我觉得这里这么做不对,这个逻辑不对,但是具体又说不出来哪里不对,反正会出问题,这里不合理”。

我们如何用这个标尺来评价问题呢?举一个笔者了解到的例子,一套志愿填报辅助系统,其中有个“一键填报”的功能,智能推荐一些志愿,但是其推荐的逻辑竟然是选择一批志愿后,经过各种一系列花里胡哨的随机操作来“看似”随机得到一些数据,推荐给用户。

我们通过这个观念去思考这个问题,难道一个志愿填报报考专家,在帮助一名学生选择要填报的院校时,会一直“随机”挑选一些院校,而不是根据某些“标准”吗?

用潘加宇老师的话来说,这样的需求既不符合系统的“愿景”,也不符合涉众的“需求”,系统的愿景绝对不是随机给用户返回一些数据,系统涉众(系统的买家)的需求也绝对不是你给他随机返回一些数据。

作为一个志愿填报系统,系统的愿景一定是帮助用户填报志愿,指标可以是“更快的填报,更好的填报,更准的填报”,涉众的需求,也一定就是你的愿景。

阿布思考法

在软件开发团队中,当有人提出新的想法时,经常会被马上否定“这太难了,这做不了”,最终得到一个平庸的、毫无竞争力的系统。学会像阿布一样思考,有助于克服普通人因资源受限而不敢展开想象的思维障碍。阿布思考法分两步:

(1)假设有充足的资源去解决问题,得到一个完美的方案;

(2)用手上现有的资源去山寨这个完美方案。

如果有一个方案,花费完美方案1%的资源,能达到完美方案20%的效果。这个方案已经是目前最好的方案了,因为它是在突破思维限制以后一步步往后退得来的。

在我们平时讨论需求或者功能时,总是疏于讨论或者思考,也许这是国内软件公司发展时间较短带来统一的弊端。我们总是因为排期,或者上来就带入思路考虑实现的复杂度,来导致最后的方案不尽人意,其实这种思考方式只是从业者经验的浓缩,每个人的想法和理由都带着自己“私货”,产品经理一般没有能力和时间考虑的过于完整,项目经理在意项目的时间,研发经理在意实现的复杂度带来的系统问题,也在意项目的时间。

但是这种考虑问题的角度,不利于公司的发展和软件价值的实现,这里就涉及到了“大部分企业中层领导的利益价值和企业整体的利益价值是矛盾的”这个比较大的问题。我们就不深入讨论了。但是站在软件系统本身的价值来看,我们应该使用阿布思考法来考虑问题,即:尽可能先得到一个完美的方案,再考虑其中无法实现的地方去山寨。

我们举一个具体例子来看待这两种思考方式的区别,假设我们现在要研发一套自动驾驶技术的全套方案,按照阿布思考法,我们会从“如何才是完美的自动驾驶”这个点来考虑问题,考虑出需要的如31套各个细节子方案后,再依次对子方案找到目前最好的替代方案,最后得到了一套“目前时代下最好的自动驾驶系统”。

如果使用通常的思考路径去考虑,会发现在一开始就寸步难行,我们不知道一套自动驾驶技术方案要包括哪些方案,就算去思考,得到的也是支离破碎的一些方案,甚至落地的时候才会发现:“哦,这里原来少了个这个东西,需要再考虑下这里怎么实现”。要么就干脆先照抄友商,然后在UI、交互等方面做点“微创新”。但是如果没有友商呢?如果你是二次创业呢?这种思考方式无法帮你领会到新的东西,也无法帮你找到终极的方向。

在机器视觉领域,相机标定是不可或缺的一步,而张氏标定法,因其灵活性,是各种标定方法中最有影响力的一种,称其为相机标定的 "奥林匹斯山" 并不为过

张正友 99年发表的论文 "
Flexible Camera Calibration by Viewing a Plane from Unknown Orientations
",在 2013年获得 ICCV 的
Helmholtz Prize
,便是对其影响的认可

鉴于该论文的广泛影响,张后来再三完善论文细节,治学严谨可见一斑。网上下载最多的是 "
A Flexible New Technique for Camera Calibration
",和获奖那篇差异不大

关于张氏标定法,不再赘述,本文将围绕"标定精度",和读者一起翻越相机标定的"奥林匹斯山"

1  相机标定

1.1  定义

已知:世界坐标系中(建在平面标定板上),几组特征点的空间坐标,以及在像素坐标系中,特征点对应的像素位置坐标

求解:相机的内参和畸变系数

标定板上特征点的空间坐标(3d),通过相机模型,与特征点的像素位置坐标(2d)关联起来,如下:

$\qquad s \begin{bmatrix} u \\ v \\ 1 \end{bmatrix}  = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix}  \begin{bmatrix} r_{11} & r_{12} & r_{13} & t_1 \\ r_{21} & r_{22} & r_{23} & t_2 \\ r_{31} & r_{32} & r_{33} & t_3 \end{bmatrix}  \begin{bmatrix} X_w \\ Y_w \\ Z_w\\ 1 \end{bmatrix} $

1.2  二维展开

相机标定的过程,就是输入几组已知的 3d 特征点坐标和提取的 2d 特征点坐标,将反投影误差构建为目标函数,在已知相机模型方程的基础上,通过最小化目标函数,得到相机模型方程的参数

开个脑洞,类比《三体》中的质子展开过程:高维度的相机标定问题,如果展开到二维空间,就是一个曲线拟合的问题,如下图,参见
Ceres Solver 的曲线拟合例程

分析相机标定的过程,得出影响标定(精度)的三个因素:1) 特征提取;2) 最优化方法;3) 相机模型

2  特征提取

以视场(对角线) 100mm 的机器视觉系统为例,根据卓越成像的最佳实践原则 #3,可知镜头的工作距离约为 200~400mm

选 1/3 英寸(对角线6mm) CIS芯片,同样根据最佳实践原则 #3,推测出镜头的理想焦距为 12~24mm

假定 CIS芯片的分辨率为 1024(H) x 768(V),则 H 方向的"像素分辨率"为 80mm/1024pixel ≈ 0.08mm/pixel

这意味着,如果提取的特征点偏差 1个像素,造成的尺寸偏差为 0.08mm,因此,特征提取的精度非常重要

2.1  标定板图案

特征提取,属于图像处理范畴,处理的是标定板在相机中的成像图片,提取的是标定板图案的特征点 (如角点、圆心等)

常用标定板的图案,有棋盘格、圆、非对称圆、圆环,以及 ChArUco 等,Halcon 公司有特定图案的 Halcon 标定板

一般而言,
圆环标定板的标定精度最高,圆次之,棋盘格最低

2.1.1  特征提取精度

参考[2]中,对三种图案标定板的各五张合成图像(非相机拍摄),添加相同的径向畸变,比较不同图案的
特征提取精度

1)  棋盘格,特征为角点,利用 OpenCV 先
findChessboardCorners()
粗定位,再
cornerSubPix()
精定位,得定位误差 6pixel

2)  圆标定板,特征为圆心,定位误差次小,为 2.6pixel

3)  圆环标定板,特征为圆环中心,定位误差最小,为 1.7pixel

对于0.08mm/pixel 的机器视觉系统,当镜头确定时(畸变系数固定),不同标定板对应的特征,提取
精度最大相差 4.3pixel,约 0.34mm

2.1.2  反投影误差

用这三种图案的各五张合成图像,来进行标定,得到反投影误差的 RMSE 分别为 0.1263,0.0517 和 0.0515

而用相机拍摄的三种图案的实际图像,进行标定后,得到反投影误差的 RMSE 分别为 0.139,0.135 和 0.115

综上,圆环标定板的标定精度,略高于圆标定板,二者均高于棋盘格标定板

2.2  迭代标定法

特征提取出现偏差,是因为在标定过程中,标定板要变换位姿,不同位姿会引起透视收缩,导致特征检测精度降低,张正友的论文里也提到过

2.2.1  偏差原因

当棋盘格旋转一定角度时,cornerSubPix() 精定位算法中的梯度,并不和边缘垂直,导致提取的角点有偏差

将标定板进行透视校正,使棋盘格正对相机,此时算法中的梯度和图像边缘垂直,角点提取无偏差

针对位姿导致的特征提取偏差,参考[2]提出了一种迭代标定法:先将图像转换为平行正对,再检测特征,重新标定,迭代直至收敛

2.2.2  方法步骤

1)给定图像的特征检测(角点、圆心或圆环中心)

2)相机标定,得到标定参数

重复以下步骤,直至收敛

3)畸变校正和透视校正(转换为平行正对图像)

4)在平行正对图像中,检测特征(模板匹配算法)

5)根据标定参数(每次迭代更新),将特征点转换回原始位置

6)相机标定,得到新的标定参数

选相机真实拍摄的图像,采用该方法标定,得到反投影误差的 RMSE,如下:

从表中看,采用迭代标定法,棋盘格的 RMSE 从 0.14 降到了 0.08,而圆和圆环的,则分别从 0.14 和 0.12 降到了 0.07 和 0.06

以上结果,是参考[2]中所列,本人未实践过,但在一家
公司公众号的文章中
(参考[4]),发现了类似迭代标定法的动图,如下:


3  优化方法

张的论文中,输入图像 ≥11幅时,标定误差显著减小,而
Bouguet 标定包
,用 20~25幅图像,图像数量越多,意味着提取的特征点也越多

实际中,受照明不均匀、标定板或镜头污染、提取算法等的限制,即使标定板转换为平行正对,提取的特征点也会有离群点

3.1  RANSAC 标定法

对于特征中离群点的剔除,常用的一种方法是 RANSAC:以反投影误差 $E_{reproj}$ 做阈值,小于的为内点,大于的为外点,不断迭代使选定的内点都满足 $<E_{reproj}$

3.1.1  方法步骤

1)张氏标定法,得到标定参数

2)计算所有特征点的反投影误差 $E_{reproj}$

3)选 $E_{reproj} < T_{reproj}$ 的所有内点,再次标定得到新的参数

重复步骤 2) 和 3),直到
所有的内点
都满足 $E_{reproj} < T_{reproj}$ (文中 T 取 2)

4)对每一幅标定图像,计算其反投影误差 $E_{img}$,设定 RANSAC 参数初值,例如,$T_{img}=1.2 E_{img}$,最大内点数 $N=\infty$,置信度 $p=0.99$,$i=1$ 等

5)将提取的特征点,在
像素位置坐标中画十字线,均分成四组
(避免四点共线)

6)四组中各选一个点,计算相机外参,综合之前的相机内参,筛选出满足 $< T_{img}$ 的内点集 $S_{in}^i$

7)如果本次筛选的 $S_{in}^i$,比之前筛选的内点数量多,则更新 $N$ 值为 $S_{in}^i$ 的内点数

8)当 $i > N$ 时,进入步骤 9),否则,令 $i=i+1$,重复步骤 5) 6) 7)

9)对每一幅图像,都执行步骤 5) 6) 7) 8),获取每一幅图像的
最大内点集
$S_{con}$

10)综合每幅图像,选出的内点集,再次标定,得到最终的标定参数

以上步骤,和 OpenCV 中的
findHomography() 函数
,在求解单应性矩阵时所用的 RANSAC 法类似,可参考之

3.1.2  测试效果

对于一幅合成的标定图像,添加高斯噪声后,采用阈值法 和 RANSAC 法,筛选出的内点集,如下:

对于一幅真实的标定图像,当因光照不均等存在离群点时,采用阈值法 和 RANSAC 法,筛选效果如下:

从统计学上分析,随着提取特征点中离群点的剔除,标定参数更逼近真实值,相机的标定精度也越来越高

由此可知,采用阈值法 和 RANSAC 法,对于大部分的离群点,都能成功的筛选出来

3.2  目标函数

3.2.1  2d 和 3d

考虑镜头的畸变,相机标定是一个非线性优化的过程,对于张氏标定法,目标函数在 2d 像平面上,如下:

可定义为像平面中检测到的特征点 $m_{u}^d$,和反投影到像平面上的特征点$\hat m_{u}^d$之间的像素差,如下:

$\quad\displaystyle\sum_{i=1}^{N} \sum_{j=1}^{L}|| m_{u,i,j}^d - \hat{m}_{u,i,j}^{d}(f_x, f_y, u_0, v_0, k_1, k_2, R_i, t_i)||^2$

在实际应用中,视觉系统测量的是 3d 空间中的特征点,2d 像平面中像素差相同的两组点对,投射到 3d 空间中的距离差反而不相同

可重定义目标函数,为检测到的特征点转换到相机坐标系 $m_c$,和反投影到相机坐标系上的特征点 $\hat m_c$ 之间的距离差,如下:

$\quad\displaystyle\sum_{i=1}^N \sum_{j=1}^L || m_{c,i,j}(R_i, t_i) - \hat{m}_{c, i, j}(f_x, f_y, u_0, v_0,k_1, k_2, R_i, t_i)||^2$

拍摄实际的图像,分别用上述两种方法进行标定,结果表明:二者的反投影误差几无差别

3.2.2  $E_1$ 和 $E_2$

目标函数变了,用原来的反投影误差来评价标定精度,不再合适,为此,引入三个新的评价指标:

$\quad E_1 = \begin{split}\frac{1}{n}\displaystyle\sum_{i=1}^{n}\sqrt{||M_{c,i} - \hat M_{c,i}||^2} \end{split}$

$\quad E_2 = \begin{split}\frac{1}{n}\displaystyle \sum_{i=1}^{n}\sqrt{||M_{c,i} - \hat L_{c,i}||^2} \end{split}$

$\quad E_3 = \begin{split}\frac{1}{m}\displaystyle \sum_{i,j=1}^{n}\sqrt{(|| M_{w,i} - M_{w,j}|| - ||\hat M_{c,i} - \hat M_{c,j}||)^2} \end{split}$

再次比较这两种标定方法,结果表明:3d 目标函数的优化精度要高于 2d 目标函数

参考

卓越成像的11条最佳实践原则

Accurate Camera Calibration using Iterative Refinement of Control Points,2009

Calibration Best Practices

计算机与机器视觉中的高精度相机标定 (下)

Accurate and robust estimation of camera parameters using RANSAC,2012

Camera Calibration Toolbox for Matlab,Jean-Yves Bougue

OpenCV Tutorials / feature2d module /
Basic concepts of the homography explained with code

A novel optimization method of camera parameters used for vision measurement,2013

Review of Calibration Methods for Scheimpflug Camera
,2018

Automatic machine vision calibration using statistical and neural network methods,2005

说明

中位数、环比、同比概念请自行百度,本文求  字段A中位数、根据字段B分组后字段A中位数、字段A环比、字段A同比、字段A中位数的环比、字段A中位数的同比。

可替换部分标黄

一、表结构如下图

查询条件为  capital_name in ('金融机构1','金融机构2'),以下查询的中位数、环比等都基于此条件;

二、求【最终金额】的【中位数】

中位数主要是利用临时变量查询,且一个sql只能查询一个字段的中位数,下面的sql对中位数做保留2位小数点处理

1 SELECT
2     @max_row_number := max( row_number ),3     ROUND( (    CASE MOD ( @max_row_number, 2)4             WHEN 0 THEN ( sum( IF ( row_number = FLOOR( @max_row_number / 2 ) OR row_number = FLOOR( @max_row_number / 2 ) + 1, final_app_amount, 0 )) / 2)5                 WHEN 1 THEN SUM( IF ( row_number = FLOOR( @max_row_number / 2 ) + 1, final_app_amount, 0 ))  END 
6             ), 2  ) ASfinal_app_amount_median7     FROM
8 (9         SELECT
10final_app_amount,11            @rank AS row_number,12             @rank := @rank + 1 
13         FROM repay_customer ASt1,14             ( SELECT @rank := 1) t215         WHERE
16            1 = 1  AND capital_name IN ( '金融机构1', '金融机构2')17         ORDER BYfinal_app_amount18 ) t3,19     ( SELECT @max_row_number := 0 ) t4

三、求【最终金额】的【分组中位数】

即根据时间,计算每月的最终金额的中位数,对结果做保留2位小数处理

1 SELECT
2 group_index,3 loan_time_credit,4     CASE MOD ( count(*), 2)5         WHEN 0 THEN     ROUND( ( sum( IF ( rank = FLOOR( group_count / 2 ) OR rank = FLOOR( group_count / 2 ) + 1, final_app_amount, 0 )) / 2  ), 2)6         WHEN 1 THEN ROUND( ( SUM( IF ( rank = FLOOR( group_count / 2 ) + 1, final_app_amount, 0 ) ) ), 2)7         END ASfinal_app_amount_median8 FROM
9 (10     SELECT
11         t3.*,12         @group_count := CASE WHEN @last_group_index = group_index THEN @group_count ELSE rank  END ASgroup_count,13         @last_group_index :=group_index14     FROM
15 (16         SELECT
17             CONCAT( DATE_FORMAT( loan_time_credit, '%Y-%m' ) ) ASgroup_index,18             DATE_FORMAT( loan_time_credit, '%Y-%m' ) ASloan_time_credit,19             final_app_amount ASfinal_app_amount,20             @rank := CASE WHEN @last_group = CONCAT( DATE_FORMAT( loan_time_credit, '%Y-%m' ) ) THEN @rank + 1 ELSE 1  END ASrank,21             @last_group := CONCAT( DATE_FORMAT( loan_time_credit, '%Y-%m'))22         FROM
23             repay_customer ASt1,24             ( SELECT @group_count := 0, @rank := 0) t225         WHERE
26             1 = 1  AND capital_name IN ( '金融机构1', '金融机构2')27         ORDER BY
28 loan_time_credit,29
final_app_amount30 ) t3,31 ( SELECT @group_count := 0, @last_group_index := 0) t432 ORDER BY 33 group_index,34 rank DESC 35 ) t536 GROUP BY 37 group_index

四、求【最终金额】和【合同金额】的环比

环比一般以月为分组条件,求环比的分组字段必须为时间字段,且只有一个时间字段;

以下sql求每月 “最终金额“ 的“和“ 的环比增长量、增长率, 和 每月 “合同金额“ 的 “平均值” 的环比增长量、增长率;

【注】此sql中计算了sum的环比和avg的环比,同理可换成 min、max,count 等;

注意
:此sql思路为根据查询条件计算出目标数据的最小时间和最大时间,罗列此区间内的所有月,再匹配分组后结果,例如, 2021-11 月里没有 金融机构1和金融机构2的数据,但结果中依然后 2021-11 这一行,目的是为了更明显的与上个月做对比;

对结果做保留2位小数点处理;

1 SELECT
2 t3.group_index,3      t3.group_index ASloan_time_credit,4      ROUND( ( ( t3.final_app_amount_sum_growth - last_final_app_amount_sum_growth )/ last_final_app_amount_sum_growth ), 2 ) ASfinal_app_amount_sum_rises,5      ROUND( ( ( t3.contract_amount_avg_growth - last_contract_amount_avg_growth )/ last_contract_amount_avg_growth ), 2 ) AScontract_amount_avg_rises,6      ROUND( ( t3.final_app_amount_sum_growth - t3.last_final_app_amount_sum_growth ), 2 ) ASfinal_app_amount_sum_growth,7      ROUND( ( t3.contract_amount_avg_growth - t3.last_contract_amount_avg_growth ), 2 ) AScontract_amount_avg_growth8  FROM
9 (10      SELECT
11          
12          @last_final_app_amount_sum_growth := CASE WHEN @last_group_index != group_index THEN     @last_final_app_amount_sum_growth ELSE t1.final_app_amount_sum_growth  END ASlast_final_app_amount_sum_growth,13          @last_contract_amount_avg_growth := CASE WHEN @last_group_index != group_index THEN @last_contract_amount_avg_growth ELSE t1.contract_amount_avg_growth  END ASlast_contract_amount_avg_growth,14          t1.*,15          @last_group_index :=group_index,16          @last_final_app_amount_sum_growth :=t1.final_app_amount_sum_growth,17          @last_contract_amount_avg_growth :=t1.contract_amount_avg_growth18      FROM
19                  (select @start_date := (select min(loan_time_credit) from repay_customer where 1=1 and capital_name IN ( '金融机构1', '金融机构2')),20                         @end_date := (select max(loan_time_credit) from repay_customer where 1=1 and capital_name IN ( '金融机构1', '金融机构2'))) t4 ,21 (22          SELECT
23 group_index,24 final_app_amount_sum_growth,25 contract_amount_avg_growth26          FROM
27 (28              SELECT
29                  DATE_FORMAT( date_sub( @start_date, INTERVAL ( @i := @i - 1 ) MONTH ), '%Y-%m' ) ASgroup_index30              FROM
31 mysql.help_topic32                  JOIN ( SELECT @i := 1) c33              WHERE
34                  help_topic_id <=(35                  TIMESTAMPDIFF( MONTH, @start_date,@end_date))36 ) dateI37              LEFT JOIN(38              SELECT
39                  DATE_FORMAT( loan_time_credit, '%Y-%m' ) ASloan_time_credit,40                  sum( final_app_amount ) ASfinal_app_amount_sum_growth,41                  avg( contract_amount ) AScontract_amount_avg_growth42              FROM
43 repay_customer44              WHERE
45                  1 = 1 
46                  AND capital_name IN (  '金融机构1', '金融机构2')47              GROUP BY
48              DATE_FORMAT( loan_time_credit, '%Y-%m' )) dataA ON dateI.group_index =dataA.loan_time_credit49 ) t1,(50          SELECT
51              @last_group_index := 0,52              @last_final_app_amount_sum_growth := 0,53              @last_contract_amount_avg_growth := 0 
54 ) t255      ) t3

五、求【最终金额】和【合同金额】的同比

同比一般与上一年比较,求同比的分组字段必须为时间字段,且只有一个时间字段;

以下sql求每月 “最终金额“ 的“和“ 的同比增长量、增长率, 和 每月 “合同金额“ 的 “平均值” 的同比增长量、增长率;

【注】此sql中计算了sum的同比和avg的同比,同理可换成 min、max,count 等;

注意
:此sql思路为根据查询条件计算出目标数据的最小时间和最大时间,罗列此区间内的所有月,再匹配分组后结果,例如, 2021-11 月里没有 金融机构1和金融机构2的数据,但结果中依然后 2021-11 这一行,目的是为了更明显的与上个月做对比;

对结果做保留2位小数点处理;

1 SELECT
2 t1.group_index,3     t1.group_index ASloan_time_credit,4     ROUND( ( ( t2.final_app_amount_sum_growth - t3.final_app_amount_sum_growth )/ t3.final_app_amount_sum_growth ), 2 ) ASfinal_app_amount_sum_rises,5     ROUND( ( ( t2.contract_amount_avg_growth - t3.contract_amount_avg_growth )/ t3.contract_amount_avg_growth ), 2 ) AScontract_amount_avg_rises,6     t2.final_app_amount_sum_growth - t3.final_app_amount_sum_growth ASfinal_app_amount_sum_growth,7     t2.contract_amount_avg_growth - t3.contract_amount_avg_growth AScontract_amount_avg_growth8 FROM
9     (select @start_date := (select min(loan_time_credit) from repay_customer where 1=1 and capital_name IN ( '金融机构1', '金融机构2')),10         @end_date := (select max(loan_time_credit) from repay_customer where 1=1and capital_name IN ( '金融机构1', '金融机构2'))) t4 ,11 (12     SELECT
13         DATE_FORMAT( date_sub( @start_date, INTERVAL ( @i := @i - 1 ) MONTH ), '%Y-%m' ) ASgroup_index14     FROM
15 mysql.help_topic16         JOIN ( SELECT @i := 1) c17     WHERE
18         help_topic_id <=(19         TIMESTAMPDIFF( MONTH,  @start_date, @end_date) )20 ) t121     LEFT JOIN(22     SELECT
23         DATE_FORMAT( loan_time_credit, '%Y-%m' ) ASgroup_index,24         DATE_FORMAT( DATE_ADD( loan_time_credit, INTERVAL - 1 YEAR ), '%Y-%m' ) ASlast_group_index,25         sum( final_app_amount ) ASfinal_app_amount_sum_growth,26         avg( contract_amount ) AScontract_amount_avg_growth27     FROM
28 repay_customer29     WHERE
30         1 = 1 
31         AND capital_name IN ( '华夏银行', '蓝海银行', '中金租')32     GROUP BY
33         DATE_FORMAT( loan_time_credit, '%Y-%m')34     ) t2 ON t1.group_index =t2.group_index35     LEFT JOIN(36     SELECT
37         DATE_FORMAT( loan_time_credit, '%Y-%m' ) ASgroup_index,38         sum( final_app_amount ) ASfinal_app_amount_sum_growth,39         avg( contract_amount ) AScontract_amount_avg_growth40     FROM
41 repay_customer42     WHERE
43         1 = 1 
44         AND capital_name IN ( '金融机构1', '金融机构2')45         AND loan_time_credit >= DATE_ADD( @start_date, INTERVAL - 1 YEAR)46         AND loan_time_credit <= DATE_ADD( @end_date, INTERVAL - 1 YEAR)47     GROUP BY
48     DATE_FORMAT( loan_time_credit, '%Y-%m')49     ) t3 ON t2.last_group_index = t3.group_index 

六、求【最终金额】中位数的环比

分组字段只能为时间且只有一个;

一个sql只能查一个字段的中位数;

对结果做保留2位小数点处理;

1 SELECT
2 t3.group_index,3     t3.group_index ASloan_time_credit,4     ROUND( ( t3.final_app_amount - t3.last_final_app_amount ), 2 ) ASfinal_app_amount_median_growth,5     ROUND( ( ( t3.final_app_amount - last_final_app_amount )/ last_final_app_amount ), 2 ) ASfinal_app_amount_median_rises6 FROM
7 (8     SELECT
9         @last_final_app_amount := CASE WHEN @last_group_index != group_index THEN     @last_final_app_amount ELSE t1.final_app_amount  END ASlast_final_app_amount,10         t1.*,11         @last_group_index :=group_index,12         @last_final_app_amount :=t1.final_app_amount13     FROM
14 (15         SELECT
16 dateI.group_index,17 final_app_amount18         FROM
19             (select @start_date := (select min(loan_time_credit) from repay_customer where 1=1 and capital_name IN ( '金融机构1', '金融机构2')),20                 @end_date := (select max(loan_time_credit) from repay_customer where 1=1 and capital_name IN (  '金融机构1', '金融机构2'))) t4 ,21 (22             SELECT
23                 DATE_FORMAT( date_sub( @start_date, INTERVAL ( @i := @i - 1 ) MONTH ), '%Y-%m' ) ASgroup_index24             FROM
25 mysql.help_topic26                 JOIN ( SELECT @i := 1) c27             WHERE
28                 help_topic_id <=(29                 TIMESTAMPDIFF( MONTH, @start_date, @end_date))30 ) dateI31             LEFT JOIN(32             SELECT
33 group_index,34             CASE
35                     MOD ( count(*), 2)36                     WHEN 0 THEN
37 (38                         sum(39                         IF
40                         ( rank = FLOOR( group_count / 2 ) OR rank = FLOOR( group_count / 2 ) + 1, final_app_amount, 0 )) / 2 
41 )42                     WHEN 1 THEN
43                     SUM(44                     IF
45                     ( rank = FLOOR( group_count / 2 ) + 1, final_app_amount, 0))46                 END ASfinal_app_amount47             FROM
48 (49                 SELECT
50                     t3.*,51                     @group_count :=
52                 CASE
53                         
54                         WHEN @last_group_index = group_index THEN
55                         @group_count ELSErank56                     END ASgroup_count,57                     @last_group_index :=group_index58                 FROM
59 (60                     SELECT
61                         DATE_FORMAT( loan_time_credit, '%Y-%m' ) ASgroup_index,62                         final_app_amount ASfinal_app_amount,63                         @rank :=
64                     CASE
65                             
66                             WHEN @last_group = DATE_FORMAT( loan_time_credit, '%Y-%m' ) THEN
67                             @rank + 1 ELSE 1 
68                         END ASrank,69                         @last_group := DATE_FORMAT( loan_time_credit, '%Y-%m')70                     FROM
71                         repay_customer ASt1,72                         ( SELECT @group_count := 0, @rank := 0) t273                     WHERE
74                         1 = 1 AND capital_name IN (  '金融机构1', '金融机构2')75                     ORDER BY
76 loan_time_credit,77 final_app_amount78 ) t3,79                     ( SELECT @group_count := 0, @last_group_index := 0) t480                 ORDER BY
81 group_index,82                     rank DESC 
83 ) t584             GROUP BY
85 group_index86             ) dataA ON dateI.group_index =dataA.group_index87 ) t1,(88         SELECT
89             @last_group_index := 0,90             @last_final_app_amount := 0 
91 ) t292     ) t3

七、求【最终金额】中位数的同比

分组字段只能为时间且只有一个;

一个sql只能查一个字段的中位数;

对结果做保留2位小数点处理;

1 SELECT
2 t1.group_index,3     t1.group_index ASloan_time_credit,4     ROUND( ( t2.final_app_amount - t3.final_app_amount ), 2 ) ASfinal_app_amount_median_growth,5     ROUND( ( ( t2.final_app_amount - t3.final_app_amount )/ t3.final_app_amount ), 2 ) ASfinal_app_amount_median_rises6 FROM
7     (select @start_date := (select min(loan_time_credit) from repay_customer where 1=1 and capital_name IN ( '金融机构1', '金融机构2')),8         @end_date := (select max(loan_time_credit) from repay_customer where 1=1 and capital_name IN ('金融机构1', '金融机构2'))) t4 ,9 (10     SELECT
11         DATE_FORMAT( date_sub( @start_date, INTERVAL ( @i := @i - 1 ) YEAR ), '%Y-%m' ) ASgroup_index12     FROM
13 mysql.help_topic14         JOIN ( SELECT @i := 1) c15     WHERE
16         help_topic_id <=(17         TIMESTAMPDIFF( MONTH, @start_date, @end_date))18 ) t119     LEFT JOIN(20     SELECT
21 group_index,22 last_year_group_index,23       CASE MOD ( count(*), 2 )  WHEN 0 THEN sum( IF ( rank = FLOOR( group_count / 2 ) OR rank = FLOOR( group_count / 2 ) + 1, final_app_amount, 0 )) / 2 
24          WHEN 1 THEN SUM( IF ( rank = FLOOR( group_count / 2 ) + 1, final_app_amount, 0 ))  END ASfinal_app_amount25     FROM
26 (27         SELECT
28             t3.*,29             @group_count :=
30         CASE
31                 
32                 WHEN @last_group_index = group_index THEN
33                 @group_count ELSErank34             END ASgroup_count,35             @last_group_index :=group_index36         FROM
37 (38             SELECT
39                 DATE_FORMAT( loan_time_credit, '%Y-%m' ) ASgroup_index,40                 DATE_FORMAT( DATE_ADD( loan_time_credit, INTERVAL - 1 MONTH ), '%Y-%m' ) ASlast_year_group_index,41 final_app_amount,42                 @rank := CASE WHEN @last_group = DATE_FORMAT( loan_time_credit, '%Y-%m' ) THEN @rank + 1 ELSE 1  END ASrank,43                 @last_group := DATE_FORMAT( loan_time_credit, '%Y-%m')44             FROM
45                 repay_customer ASt1,46                 ( SELECT @group_count := 0, @rank := 0) t247             WHERE
48                 1 = 1      AND capital_name IN ( '金融机构1', '金融机构2')49             ORDER BY
50 loan_time_credit,51 final_app_amount52 ) t3,53             ( SELECT @group_count := 0, @last_group_index := 0) t454         ORDER BY
55 group_index,56             rank DESC 
57 ) t558     GROUP BY
59 group_index60     ) t2 ON t1.group_index =t2.group_index61     LEFT JOIN(62     SELECT
63 group_index,64       CASE     MOD ( count(*), 2)65           WHEN 0 THEN sum( IF ( rank = FLOOR( group_count / 2 ) OR rank = FLOOR( group_count / 2 ) + 1, final_app_amount, 0 )) / 2 
66             WHEN 1 THEN SUM( IF ( rank = FLOOR( group_count / 2 ) + 1, final_app_amount, 0))67           END ASfinal_app_amount68     FROM
69 (70         SELECT
71             t3.*,72             @group_count := CASE WHEN @last_group_index = group_index THEN @group_count ELSE rank  END ASgroup_count,73             @last_group_index :=group_index74         FROM
75 (76             SELECT
77                 DATE_FORMAT( loan_time_credit, '%Y-%m' ) ASgroup_index,78 final_app_amount,79                 @rank := CASE WHEN @last_group = DATE_FORMAT( loan_time_credit, '%Y-%m' ) THEN @rank + 1 ELSE 1  END ASrank,80                 @last_group := DATE_FORMAT( loan_time_credit, '%Y-%m')81             FROM
82                 repay_customer ASt1,83                 ( SELECT @group_count := 0, @rank := 0) t284             WHERE
85                 1 = 1  AND capital_name IN ('金融机构1', '金融机构2')86                 AND loan_time_credit >=  DATE_ADD( @start_date, INTERVAL - 1 YEAR)87                 AND loan_time_credit <=  DATE_ADD( @end_date, INTERVAL - 1 YEAR)88             ORDER BY
89 loan_time_credit,90 final_app_amount91 ) t3,92             ( SELECT @group_count := 0, @last_group_index := 0) t493         ORDER BY
94 group_index,95             rank DESC 
96 ) t597     GROUP BY
98 group_index99     ) t3 ON t2.last_year_group_index = t3.group_index

八 完

太不容易了我!

基于ASP.NET Core SignalR 可以实现客户端和服务器之间进行即时通信。本篇随笔介绍一些SignalR的基础知识,以及结合对SqlSugar的开发框架的支持,实现SignalR的多端处理整合,从而实现Winform客户端,基于Vue3+ElementPlus的BS端整合,后面也可以实现对移动端的SignalR的整合通讯。

适合 SignalR 的应用场景:

  • 需要从服务器进行高频率更新的应用。 示例包括游戏、社交网络、投票、拍卖、地图和 GPS 应用。
  • 仪表板和监视应用。
  • 协作应用。 协作应用的示例包括白板应用和团队会议软件。
  • 需要通知的应用。 社交网络、电子邮件、聊天、游戏、旅行警报和很多其他应用都需使用通知。

SignalR 自动选择服务器和客户端能力范围内的最佳传输方法,如
WebSockets
、Server-Sent Events、长轮询。Hub 是一种高级管道,允许客户端和服务器相互调用方法。 SignalR 自动处理跨计算机边界的调度,并允许客户端调用服务器上的方法,反之亦然。SignalR 提供两个内置中心协议:基于 JSON 的文本协议和基于 MessagePack 的二进制协议。

客户端负责通过
HubConnection
对象建立到服务器终结点的连接。 Hub 连接在每个目标平台中表示:

当中心连接实例成功启动后,消息可以自由地双向流动。 用户可以自由地将通知发送到服务器,以及从服务器接收通知。 客户端是任何已连接的应用程序,例如(但不限于)Web 浏览器、移动应用或桌面应用。

1、SignalR服务端

在.net core的Web API上,我们首先需要注册SignalR的服务,然后创建对应的Hub进行使用。一般可以在启动类中添加如下代码即可。

builder.Services.AddSignalR();//即时通讯
app.UseEndpoints(endpoints=>{//注册集线器
    endpoints.MapHub<OnlineUserHub>("/hubs/onlineUser");
});

定义集线器只需要继承
Hub

Hub<TStrongType>
泛型基类即可。

public classChatHub : Hub
{
public async Task SendMessage(string user, stringmessage)=> await Clients.All.SendAsync("ReceiveMessage", user, message);
}

泛型强类型方法是使用
Hub<T>
的强类型
Hub
类。在以下示例中
ChatHub
,客户端方法已提取到名为 的
IChatClient
接口中:

public interfaceIChatClient
{
Task ReceiveMessage(
string user, stringmessage);
}

此接口可用于将前面的
ChatHub
示例重构为强类型:

public class ChatHub : Hub<IChatClient>{public async Task SendMessage(string user, stringmessage)=> awaitClients.All.ReceiveMessage(user, message);public async Task SendMessageToCaller(string user, stringmessage)=> awaitClients.Caller.ReceiveMessage(user, message);public async Task SendMessageToGroup(string user, stringmessage)=> await Clients.Group("SignalR Users").ReceiveMessage(user, message);
}

这样Clients的对象都具备了接口定义的
ReceiveMessage
方法调用,实际这个就是客户端的方法。

使用
Hub<IChatClient>
可以对客户端方法进行编译时检查。 这可以防止使用字符串引起的问题,因为
Hub<T>
只能提供对 接口中定义的方法的访问权限。 使用强类型
Hub<T>
会禁止使用
SendAsync

Hub服务端中心

public interfaceIClient
{
Task
<string>GetMessage();
}
public class ChatHub : Hub<IClient>{public async Task<string> WaitForMessage(stringconnectionId)
{
string message = awaitClients.Client(connectionId).GetMessage();returnmessage;
}
}

.NET 客户端

客户端在其
.On(...)
处理程序中返回结果,如下所示:

hubConnection.On("GetMessage", async () =>{
Console.WriteLine(
"Enter message:");var message = awaitConsole.In.ReadLineAsync();returnmessage;
});

Typescript 客户端

hubConnection.on("GetMessage", async () =>{
let promise
= new Promise((resolve, reject) =>{
setTimeout(()
=>{
resolve(
"message");
},
100);
});
returnpromise;
});

Java 客户端

hubConnection.onWithResult("GetMessage", () ->{return Single.just("message");
});

在框架中整合SignalR的Hub的时候,我们定义一个接口IOnlineUserHub,以便强类型对客户端接口方法的调用,减少错误。

然后在定义一个Hub的对象类,如下所示 。

public class OnlineUserHub : Hub<IOnlineUserHub>{private readonlyIOnlineUserService _onlineUserService;private readonly IHubContext<OnlineUserHub, OnlineUserHub>_chatHubContext;publicOnlineUserHub(IOnlineUserService onlineUserService,
IHubContext
<OnlineUserHub, IOnlineUserHub>onlineUserHubContext)
{
_onlineUserService
=onlineUserService;
_chatHubContext
=onlineUserHubContext;
}
}

对象Hub<T>本身可以通过注入一个
IHubContext<OnlineUserHub, OnlineUserHub>
接口来获得对它的调用,如上面构造函数所示。该Hub一般还需要重写连接和断开的处理操作,如下代码所示。

如对于用户的SignalR连接发起,我们需要判断用户的令牌及相关身份信息,如果成功,则通过给客户端提供在线用户列表。

        /// <summary>
        ///连接后处理/// </summary>
        /// <returns></returns>
        public override asyncTask OnConnectedAsync()
{
var httpContext =Context.GetHttpContext();var token = httpContext!.Request.Query["access_token"];if (string.IsNullOrWhiteSpace(token)) return;

................
//向客户端提供在线用户信息 await _chatHubContext.Clients.Groups(groupName).OnlineUserList(newOnlineUserList
{
ConnectionId
=user.ConnectionId,
RealName
= user.RealName + $"({client.UA.Family})", //加上实际终端 Online = true,
UserList
=userList.Items.ToList()
});
//更新在线用户缓存 awaitRedisHelper.SetAsync(CacheConst.KeyOnlineUser, userList.Items.ToList());
}

上下文对象


Hub
包含一个
Context
属性,该属性包含以下属性以及有关连接的信息:

属性 说明
ConnectionId 获取连接的唯一 ID(由 SignalR 分配)。 每个连接都有一个连接 ID。
UserIdentifier 获取
用户标识符
。 默认情况下,SignalR 使用与连接关联的
ClaimsPrincipal
中的
ClaimTypes.NameIdentifier
作为用户标识符。
User 获取与当前用户关联的
ClaimsPrincipal
Items 获取可用于在此连接范围内共享数据的键/值集合。 数据可以存储在此集合中,会在不同的中心方法调用间为连接持久保存。
Features 获取连接上可用的功能的集合。 目前,在大多数情况下不需要此集合,因此未对其进行详细记录。
ConnectionAborted 获取一个
CancellationToken
,它会在连接中止时发出通知。

Hub.Context
还包含以下方法:

方法 说明
GetHttpContext 返回
HttpContext
连接的 ;如果连接未与 HTTP 请求关联,
null
则返回 。 对于 HTTP 连接,请使用此方法获取 HTTP 标头和查询字符串等信息。
Abort 中止连接。

客户端对象


Hub
包含一个
Clients
属性,该属性包含以下用于服务器和客户端之间通信的属性:

属性 说明
All 对所有连接的客户端调用方法
Caller 对调用了中心方法的客户端调用方法
Others 对所有连接的客户端调用方法(调用了方法的客户端除外)

Hub.Clients
还包含以下方法:

方法 说明
AllExcept 对所有连接的客户端调用方法(指定连接除外)
Client 对连接的一个特定客户端调用方法
Clients 对连接的多个特定客户端调用方法
Group 对指定组中的所有连接调用方法
GroupExcept 对指定组中的所有连接调用方法(指定连接除外)
Groups 对多个连接组调用方法
OthersInGroup 对一个连接组调用方法(不包括调用了中心方法的客户端)
User 对与一个特定用户关联的所有连接调用方法
Users 对与多个指定用户关联的所有连接调用方法

这样我们Hub里面定义的方法,就可以利用这些对象来处理了。

        /// <summary>
        ///前端调用发送方法(发送信息给所有人)/// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        public asyncTask ClientsSendMessagetoAll(MessageInput message)
{
await_chatHubContext.Clients.All.ReceiveMessage(message);
}
/// <summary> ///前端调用发送方法(发送消息给除了发送人的其他人)/// </summary> /// <param name="message"></param> /// <returns></returns> public asyncTask ClientsSendMessagetoOther(MessageInput message)
{
var onlineuserlist = RedisHelper.Get<List<OnlineUserInfo>>(CacheConst.KeyOnlineUser);var user = onlineuserlist.Where(x => x.UserId ==message.UserId).ToList();if (user != null)
{
await _chatHubContext.Clients.AllExcept(user[0].ConnectionId).ReceiveMessage(message);
}
}

基于IHubContext的接口,我们也可以定义一个常规的接口函数,用于在各个服务类中调用Hub处理函数

    /// <summary>
    ///封装的SignalR的常规处理实现/// </summary>
    public class HubContextService : BaseService, IHubContextService

这样在服务端,注册服务后,可以使用这个自定义服务类的处理逻辑。

//使用HubContextService服务接口
builder.Services.AddSingleton<IHubContextService, HubContextService>();

可以供一些特殊的控制器来使用Hub服务接口,如登录后台的时候,实现强制多端下线的处理方式。

    /// <summary>
    ///登录获取令牌授权的处理/// </summary>
    [Route("api/[controller]")]
[ApiController]
public classLoginController : ControllerBase
{
private readonly IHubContextService _hubContextService;
        /// <summary>
        ///登录授权处理/// </summary>
        /// <returns></returns>
[AllowAnonymous]
[HttpPost]
[Route(
"authenticate")]public async Task<AuthenticateResultDto>Authenticate(LoginDto dto)
{
var authResult = newAuthenticateResultDto();
................
var loginResult = await this._userService.VerifyUser(dto.LoginName, dto.Password, ip);if (loginResult != null && loginResult.UserInfo != null)
{
var userInfo =loginResult.UserInfo;

...............
//单用户登录 await this._hubContextService.SignleLogin(userInfo.Id.ToString());
}
else{
authResult.Error
= loginResult?.ErrorMessage;
}
returnauthResult;
}

2、SignalR客户端

.net客户端在对接Hub中心服务端的时候,需要添加Microsoft.AspNetCore.SignalR.Client的引用。

Install-Package Microsoft.AspNetCore.SignalR.Client

若要建立连接,请创建
HubConnectionBuilder
并调用
Build
。 在建立连接期间,可以配置中心 URL、协议、传输类型、日志级别、标头和其他选项。 可通过将任何
HubConnectionBuilder
方法插入
Build
中来配置任何必需选项。 使用
StartAsync
启动连接。

usingSystem;usingSystem.Threading.Tasks;usingSystem.Windows;usingMicrosoft.AspNetCore.SignalR.Client;namespaceSignalRChatClient
{
public partial classMainWindow : Window
{
HubConnection connection;
publicMainWindow()
{
InitializeComponent();

connection
= newHubConnectionBuilder()
.WithUrl(
"http://localhost:53353/ChatHub")
.Build();

connection.Closed
+= async (error) =>{await Task.Delay(new Random().Next(0,5) * 1000);awaitconnection.StartAsync();
};
}
private async void connectButton_Click(objectsender, RoutedEventArgs e)
{
connection.On
<string, string>("ReceiveMessage", (user, message) =>{this.Dispatcher.Invoke(() =>{var newMessage = $"{user}: {message}";
messagesList.Items.Add(newMessage);
});
});
try{awaitconnection.StartAsync();
messagesList.Items.Add(
"Connection started");
connectButton.IsEnabled
= false;
sendButton.IsEnabled
= true;
}
catch(Exception ex)
{
messagesList.Items.Add(ex.Message);
}
}
private async void sendButton_Click(objectsender, RoutedEventArgs e)
{
try{await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
}
catch(Exception ex)
{
messagesList.Items.Add(ex.Message);
}
}
}
}

可以将
HubConnection
配置为对
HubConnectionBuilder
使用
WithAutomaticReconnect
方法来自动重新连接。 默认情况下,它不会自动重新连接。

HubConnection connection= newHubConnectionBuilder()
.WithUrl(
new Uri("http://127.0.0.1:5000/chathub"))
.WithAutomaticReconnect()
.Build();

在没有任何参数的情况下,
WithAutomaticReconnect()
将客户端配置为在每次尝试重新连接之前分别等待 0、2、10 和 30 秒,在四次尝试失败后停止。

为了测试Winform客户端对服务端的连接,我们可以新建一个小案例Demo,来测试信息处理的效果。

创建一个测试的窗体如下所示(实际测试效果)。

创建连接Hub中心的代码如下所示。

        /// <summary>
        ///初始化服务连接/// </summary>
        private asyncTask InitHub()
{
........
//创建连接对象,并实现相关事件 var url = serverUrl + $"/hubs/onlineUser?access_token={authenticateResultDto.AccessToken}";
hubConnection
= newHubConnectionBuilder()
.WithUrl(url)
.WithAutomaticReconnect(
new[] { TimeSpan.Zero, TimeSpan.Zero, TimeSpan.FromSeconds(10) }) //自动连接 .Build();//接收实时信息 hubConnection.On<MessageInput>("ReceiveMessage", ReceiveMessage);//连接上处理在线用户 hubConnection.On<OnlineUserList>("OnlineUserList", OnlineUserList);//客户端收到服务关闭消息 hubConnection.On("ForceOffline", async (ForceOfflineInput data) =>{awaitCloseHub();
});
try{//开始连接 awaithubConnection.StartAsync();var content = $"连接到服务器:{serverUrl}";
AddSystemMessage(content);
}
catch(Exception ex)
{
Console.WriteLine(ex.StackTrace);
var content = $"服务器连接失败:{ex.Message}";
AddSystemMessage(content);

InitControlStatus(
false);return;
}
}

我们可以看到,客户端接收服务端的消息处理,通过下面代码进行处理。

//接收实时信息
hubConnection.On<MessageInput>("ReceiveMessage", ReceiveMessage);//连接上处理在线用户
hubConnection.On<OnlineUserList>("OnlineUserList", OnlineUserList);//客户端收到服务关闭消息
hubConnection.On("ForceOffline", async (ForceOfflineInput data) =>

对于消息的接收处理,我们把它收到一个本地的集合列表中,然后统一处理即可。

/// <summary>
///消息处理/// </summary>
/// <param name="data">JSON字符串</param>
private voidReceiveMessage(MessageInput data)
{
if (this.onlineUser != null)
{
var info = newMessageInfo(data);
.............
TryAddMessage(ownerId, info);
BindTree();
}
}

发送消息的时候,我们根据指向不同的用户,构造对应的消息体发送(调用服务端Hub接口)即可,调用通过
InvokeAsync
处理,接收相应的对象。

private async void BtnSendMessage_Click(objectsender, EventArgs e)
{
if (txtMessage.Text.Length == 0)return;var message = newMessageInput()
{
Title
= "消息",
Message
=txtMessage.Text,
MessageType
=MessageTypeEnum.Info,
UserId
= this.toId,
UserIds
= new List<string>()
};
//判断发送人,是单个发送,还是广播发送所有人 var methodName = !string.IsNullOrEmpty(this.toId) ? "ClientsSendMessage" : "ClientsSendMessagetoAll";awaithubConnection.InvokeAsync(methodName, message);}

测试功能正常,我们就可以把窗体整合到Winform端的主体界面中了。

在Winform端的登陆处理的时候,我们把SignarR的主要处理逻辑放在全局类GlobalControl 中,方便调用,并定义好几个常用的对象,如连接,在线用户信息,消息列表等。

并通过定义事件的方式,在消息变化的时候,通知界面进行更新处理。

public event EventHandler<MessageInfo> SignalRMessageChanged;

因此我们可以在主界面上提供一个入口,供消息的处理操作。

主窗体在界面初始化的时候,调用一下全局类的初始化SignalR的Hub连接即可。

        /// <summary>
        ///初始化SignalR的处理/// </summary>
        private async voidInitSignalR()
{
awaitPortal.gc.InitHub();
}

这样就会根据相应的信息,实现HubConnection的初始化操作了,而且这个连接的生命周期是伴随整个应用的出现而出现的。

打开就可以展示在线用户,并可以和系统相关用户发送实时消息了。如果可以,我们也可以把消息存储在数据库端,然后离线也可以收到存储起来,供下次登录后进行查看。

窗体可以对SignalR消息进行实时的更新相应,通过事件的实现。

    public partial classFrmSignalClient : BaseDock
{
publicFrmSignalClient()
{
InitializeComponent();

Portal.gc.SignalRMessageChanged
+=SignalRMessageChanged;
}

由于篇幅的原因,后面在介绍在Vue3+Element的BS端中实现对SignalR消息整合的处理操作。

观点:关于 AIGC

最近看的这块挺多。分享下我对 AIGC 的一些观点

AIGC(AI Generated Content)是由 AI 生成的内容。我认为的内容很多,文字、图片、视频、音频、3D 等等

观点 1: AIGC 不应该卷互联网行业,for 工业 for 生产。比如服装来源于设计稿,应该由 AI 辅助,让服装设计 plus 下

观点 2: AIGC 能预测但不能创造,创造新的有价值的内容。保持对创新、发明的敬畏和支持

能力:关于沟通

任何一切合作的基础是沟通

线上聊天千万遍,不如线下见一面

那怎么沟通呢?你看到的书基本都是理论理论理论,沟通最重要的是

  • 真诚:知之为知之不知为不知,宁说三句真,不带半句假。真诚是沟通的必杀技。
  • 利他:自损度小情况下,考虑别人的利益为先,往往会有更大的回报。
  • 常说是的,对,尤其在别人表达的时候;而不是我知道,我都明白...
  • 个性:这是你的画像、你的人格,比如幽默、会说几句 zang 话等等。接受它,保持它,无伤大雅。

能力:关于内容

内容输出,最大的差异化有几点:

  • 文字透露出那个角色,是内容输出根源的问题。AI 永远代替不了活生生的一个角色,只是演员
  • 创造:都知道 content is king!但 new content is always the top king!

能力:专注专属技能

为什么要专注专属技能?因为

  • 这样自己才能通过技能,输出小作品,商业小作品就能赚到钱
  • 这样周围的才会慕名你的作品和技能,合作就会来,就能赚钱

我的专注如下,那你呢?

  • 产研能力
  • 流量能力:谷歌 SEO

心态:赚多少钱会让你有安全感?

file
这段时间,因为 ChatGPT 流量突增,如图有朋友这波达到了日入万元。但我好想没有以前那样焦虑了,反而为他们开心,还会幽默道:“今天按时薪算,你们都比我高多了~”

我也问自己问什么?难道我不想赚钱吗?

答案是当然想,为什么的原因总结如下:

  • 我十分喜欢赚钱:我也认为
    赚钱,并不是坏事。追求钱也跟人品无关,但是,贪欲是有害的。
    知足而常乐,天下没有免费的午餐,每个人都需要为贪欲买单
  • 我十分自信:我也能赚到他们那个水平,只是时间会给我答案
  • 我深知:
    长期主义,复利,才是我追的方向,但需要时间
  • 我深知:
    赚钱是解决财务独立问题,满足物质需求,而不是源源不断不可控的各种欲望
  • 我深知:
    财务自由的状态,就是保持原有的生活方式的同时,金钱远远超过你实际的需求和欲望

《纳瓦尔宝典》有句话,很经典:

钱不是万恶之源
钱本身无罪的,真正有害的是对钱的贪欲

原文

https://bysocket.com/saas-think-202302/

出处:公号「程序员泥瓦匠」
博客:
https://bysocket.com/

内容涵盖 Java 后端技术、Spring Boot、Spring Cloud、微服务架构、运维开发、系统监控等相关的研究与知识分享。