wenmo8 发布的文章

一:背景

1. 讲故事

上个月在社区写的文章比较少,一直关注的朋友应该知道那段时间被狗咬了以及一些琐事处理,所以手头上也攒了不少需要分享的案例,这段时间比较空闲,逐个给大
家做个分享吧,刚好年后为新版的
.NET高级调试训练营
做案例储备,所以抓紧时间疯狂输出吧!

这次生产事故的dump是训练营里一位朋友给到我的,由于朋友没有分析出来,让我帮忙看看,毕竟我的修车经验相对来说更丰富一些,算是他们背后坚实的保障吧,话不多说上windbg说话。

二:WinDbg分析

1. 为什么会崩溃

由于windbg默认自动定位到崩溃的线程,而崩溃的dump重点是观察它的崩溃前上下文,这里使用
.ecxr

k
命令,输出参考如下:


0:000> .ecxr
eax=00000000 ebx=4d6d8360 ecx=00000003 edx=00000000 esi=4d6f0ca0 edi=4d6f0c74
eip=71a567c7 esp=026fd834 ebp=026fd83c iopl=0         nv up di pl nz na po nc
cs=0000  ss=0000  ds=0000  es=0000  fs=0000  gs=0000             efl=00000000
System_Windows_Forms_ni!System.Windows.Forms.ImageList.ImageCollection.SetKeyName+0x1b:
71a567c7 cc              int     3

0:000> k
  *** Stack trace for last set context - .thread/.cxr resets it
 # ChildEBP RetAddr      
00 026fd83c 0c2c4e7e     System_Windows_Forms_ni!System.Windows.Forms.ImageList.ImageCollection.SetKeyName+0x1b
01 026fe474 0c2c063b     xxx!xxx.MainForm.InitializeComponent+0x198e
02 026fe488 095cb9de     xxx!xxx.MainForm..ctor+0x5fb
03 026fe4e4 0da5bc7a     xxx!xxx.LoginForm.button_OK_Click+0x52e
04 026fe4f8 71a38bdf     xxx!xxx.LoginForm.LoginForm_Load+0x9a
05 026fe528 710b325a     System_Windows_Forms_ni!System.Windows.Forms.Form.OnLoad+0x2f
...

从卦象看是崩溃在
System.Windows.Forms.ImageList.ImageCollection.SetKeyName
方法上,很显然这个方法属于微软的SDK底层库,不管怎么说是一个托管异常,既然是托管异常我们可以用
!t
观察到具体的崩溃信息。


0:000> !t
ThreadCount:      26
UnstartedThread:  0
BackgroundThread: 12
PendingThread:    0
DeadThread:       13
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1  534 0299e700     a6028 Preemptive  4D6F0EFC:00000000 02997bd0 0     STA System.IndexOutOfRangeException 4d6f0ca0
   2    2  5b4 029af278     2b228 Preemptive  00000000:00000000 02997bd0 0     MTA (Finalizer) 
   3    6  ff0 02a60eb0   102a228 Preemptive  00000000:00000000 02997bd0 0     MTA (Threadpool Worker) 
   ...

0:000> !pe
Exception object: 4d6f0ca0
Exception type:   System.IndexOutOfRangeException
Message:          Index was outside the bounds of the array.
InnerException:   <none>
StackTrace (generated):
    SP       IP       Function
    026FD834 71A567C7 System_Windows_Forms_ni!System.Windows.Forms.ImageList+ImageCollection.SetKeyName(Int32, System.String)+0x33e157

StackTraceString: <none>
HResult: 80131508

从卦象看非常奇怪,怎么底层库中抛了一个
数组索引越界
异常?难道是底层的bug?一般来说这些代码都是铜墙铁壁,固若金汤,坚如磐石,稳如泰山,无懈可击,不可能有如此低级的bug。。。

2. 真的是底层库bug吗?

要想寻找答案,可以根据线程栈上的函数寻找底层源码,从源码上寻找答案,修剪后的代码如下:


        private void InitializeComponent()
        {
            this.imageList_btnbg.ImageStream = (System.Windows.Forms.ImageListStreamer)resources.GetObject("imageList_btnbg.ImageStream");
            this.imageList_btnbg.TransparentColor = System.Drawing.Color.Transparent;
            this.imageList_btnbg.Images.SetKeyName(0, "normal-main.bmp");
            this.imageList_btnbg.Images.SetKeyName(1, "focus-main.bmp");
            this.imageList_btnbg.Images.SetKeyName(2, "select-main.bmp");
            this.imageList_btnbg.Images.SetKeyName(3, "gray-main.bmp");
            this.imageList_btnbg.Images.SetKeyName(4, "down_1.png");
            this.imageList_btnbg.Images.SetKeyName(5, "down_2.png");
            this.imageList_btnbg.Images.SetKeyName(6, "down_3.png");
            this.imageList_btnbg.Images.SetKeyName(7, "up_1.png");
            this.imageList_btnbg.Images.SetKeyName(8, "up_2.png");
            this.imageList_btnbg.Images.SetKeyName(9, "up_3.png");
        }

        public void SetKeyName(int index, string name)
        {
            if (!IsValidIndex(index))
            {
                throw new IndexOutOfRangeException();
            }
            if (imageInfoCollection[index] == null)
            {
                imageInfoCollection[index] = new ImageInfo();
            }

            ((ImageInfo)imageInfoCollection[index]).Name = name;
        }

        private bool IsValidIndex(int index)
        {
            if (index >= 0)
            {
                return index < Count;
            }
            return false;
        }

        public int Count
        {
            get
            {
                if (owner.HandleCreated)
                {
                    return SafeNativeMethods.ImageList_GetImageCount(new HandleRef(owner, owner.Handle));
                }
                int num = 0;
                foreach (Original original in owner.originals)
                {
                    if (original != null)
                    {
                        num += original.nImages;
                    }
                }
                return num;
            }
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        [SRDescription("ImageListHandleCreatedDescr")]
        public bool HandleCreated => nativeImageList != null;

仔细通读卦中的代码逻辑,看样子是
IsValidIndex()=false
导致的手工 IndexOutOfRangeException 异常,而
IsValidIndex()=false
是由于
index < Count
的条件成立,后面的 Count 是取自
ImageList_GetImageCount
或者
owner.originals
值。

代码逻辑我们是分析清楚了,接下来就是看汇编来分析下这个dump的现状,入手点就是从 index 值入手,即对
InitializeComponent()
方法进行反汇编。


0:000> !clrstack
OS Thread Id: 0x534 (0)
Child SP       IP Call Site
026fd784 771316bc [HelperMethodFrame: 026fd784] 
026fd834 71a567c7 System.Windows.Forms.ImageList+ImageCollection.SetKeyName(Int32, System.String)
026fd848 0c2c4e7e xxx.MainForm.InitializeComponent()
026fe47c 0c2c063b xxx.MainForm..ctor()
026fe490 095cb9de xxx.LoginForm.button_OK_Click(System.Object, System.EventArgs)
...

0:000> !U /d 0c2c4e7e
Normal JIT generated code
xxx.MainForm.InitializeComponent()
Begin 0c2c34f0, size 5ded
...
0c2c4e62 e8d9b5d864      call    System_Windows_Forms_ni+0x160440 (71050440) (System.Windows.Forms.ImageList.get_Images(), mdToken: 06002599)
0c2c4e67 898514f5ffff    mov     dword ptr [ebp-0AECh],eax
0c2c4e6d ff35f0a07f05    push    dword ptr ds:[57FA0F0h] ("normal-main.bmp")
0c2c4e73 8bc8            mov     ecx,eax
0c2c4e75 33d2            xor     edx,edx
0c2c4e77 3909            cmp     dword ptr [ecx],ecx
0c2c4e79 e8f2374565      call    System_Windows_Forms_ni!System.Windows.Forms.ImageList.ImageCollection.SetKeyName (71718670)
>>> 0c2c4e7e 8b8e74020000    mov     ecx,dword ptr [esi+274h]
...

从卦象看,尼玛。。。执行第一个
SetKeyName(0, "normal-main.bmp");
就异常啦,这就说明那个 Count=0,无语了,为什么 Count=0 呢? 接下来寻找Count数据源ImageCollection 集合,可以从线程栈中寻找,使用
!dso
命令即可。

0:000> !dso
OS Thread Id: 0x534 (0)
ESP/REG  Object   Name
026FD774 4d6f0c74 System.Windows.Forms.ImageList+ImageCollection
026FD778 4d6f0ca0 System.IndexOutOfRangeException
...

0:000> !do 4d6f0c74
Name:        System.Windows.Forms.ImageList+ImageCollection
MethodTable: 71120ff0
EEClass:     70f230ec
Size:        20(0x14) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
7111ecc0  4003916        4 ...s.Forms.ImageList  0 instance 4d6d97b0 owner
72d909dc  4003917        8 ...ections.ArrayList  0 instance 4d6f0c88 imageInfoCollection
72d8df5c  4003918        c         System.Int32  1 instance       -1 lastAccessedIndex
0:000> !DumpObj /d 4d6d97b0
Name:        System.Windows.Forms.ImageList
...
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
71121b0c  4001013       10 ...t+NativeImageList  0 instance 4d6f0c40 nativeImageList
728e15a0  4001019       1c ...Collections.IList  0 instance 00000000 originals

根据卦中的
nativeImageList

originals
再配合源代码,应该就是祸首
SafeNativeMethods.ImageList_GetImageCount
方法返回 0 导致的,先观察一下它的签名。


[DllImport("comctl32.dll")]
public static extern int ImageList_GetImageCount(HandleRef himl);

从签名看这是C++写的外部方法,这就沃草了。。。我总不能用 ida 去捋这里面的逻辑吧。。。到这里貌似已经快要撞到南墙了。。。有点慌了。

3. 天要绝人之路吗

经过短暂的恍恍惚惚之后,我突然灵光一现,尼玛这是32bit的内存地址,是不是2G的空间不够用哦?刚好 ImageList_GetImageCount 是一个关于图片的UI控件,用了底层的COM资源,会不会真的是空间不足导致的?有了这个想法之后赶紧
!address -summary
观察提交内存。


0:000> !address -summary
...
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_COMMIT                             1933          6e768000 (   1.726 GB)  93.52%   86.30%
MEM_FREE                                631           9e01000 ( 158.004 MB)            7.72%
MEM_RESERVE                             607           7a87000 ( 122.527 MB)   6.48%    5.98%
...

尼玛。。。卦象中的
MEM_COMMIT=1.72G, %ofBusy= 93.52%
早已超过了1.2G的临界值,终于真相大白。。。

解决办法就比较简单了,开启大地址,让程序吃 4G 的内存,后来朋友反馈这个问题已不再出来。。。

三:总结

分析完这个dump之后其实我挺感慨的,人生也如此dump一样,在真相和假象之间不断的交织穿梭,有些人走出来了,有些人永远留在了里面。。。
图片名称

当前是vue3+ts版本的封装

vue3+js版本请求封装可参考
https://www.cnblogs.com/lovejielive/p/14343619.html

token无感刷新,可自行删除 requset.ts 中 ts 相关数据恢复vue2版本

先在目录下创建 utils 和 common 这2个文件夹

img

utils 是存放工具类的,common 用来放置常用方法的

之后在utils 中创建 requset.ts 用来放置 uni.request 的请求方法,无感刷新。

1.common 文件创建 operate.ts + api.ts

主要用来放置 ,请求接口地址,一些全局请求数据,判断是否登录。

配置全局消息提示框,模拟对话框方法,方便调用

operate.ts 代码如下:


import store from '@/store/index'exportdefault{//接口
    api: function() {
let url
= '' //#ifdef MP-WEIXIN || MP-ALIPAY let version =uni.getAccountInfoSync().miniProgram.envVersion;switch(version) {case "develop": //开发预览版 url = '' break;case 'trial': //体验版 url = '' break;case 'release': //正式版 url = '' break;default: //未知,默认调用正式版 url = '' break;
}
//#endif //#ifdef H5 || APP-PLUS if (process.env.NODE_ENV === 'development') {//console.log('开发环境') url = ''}else{//console.log('生产环境') url = ''}//#endif returnurl
},
//共同请求参数 commonBeg: function() {return{
Authorization:
this.isToken(),
}
},
//是否已注册(登录状态) isLogin: function() {returnstore.state.user.hasLodin;
},
//获取用户token isToken: function() {if (store.state.user.accessToken != '') {return 'Bearer ' +store.state.user.accessToken;
}
return '';
},
//消息提示框 toast: function(options : any) {
uni.showToast({
title: options.title,
duration: options.duration
|| 2000,
icon: options.icon
|| "none"});
},
//模拟对话框 showModal: function(matter : any) {return new Promise((resolve, _reject) =>{
uni.showModal({
title: matter.title
|| '',
content: matter.content
|| '',//是否显示取消按钮,默认为 true showCancel: matter.showCancel,//取消按钮的文字,默认为"取消" cancelText: matter.cancelText || "取消",//取消按钮的文字颜色,默认为"#000000" cancelColor: matter.cancelColor || "#000000",//确定按钮的文字,默认为"确定" confirmText: matter.confirmText || "确定",/*确定按钮的文字颜色,H5平台默认为"#007aff",
微信小程序平台默认为"#576B95",
百度小程序平台默认为 "#3c76ff"
*/ //confirmColor: matter.confirmColor || '#576B95', success: (res) =>{if(res.confirm) {//console.log('用户点击确定'); resolve(res)
}
//if (res.cancel) { //console.log('用户点击取消'); //reject(res.cancel) //} }
})
})
}
}

operate.ts

api.ts 代码如下


import {
request
} from
'@/utils/requset' //手机密码登录 export const text = function(data : any) {returnrequest({
url:
"/pai/api/pai",
method:
"POST",
hideLoading:
true,
data: data,
})
}
/*使用方法: 在请求页面中调用

1.先导入本页面
import {text} from '@/common/api'

2.在methods 中 调用:
text().then((res) => {
console.log(res);
})
*/

api.ts

2.utils 中创建 requset.ts

配置
uni.request
统一请求,
uni.getNetworkType
判断当前网络状态

通过
uni.addInterceptor
拦截器,实现请求前后的数据监听(该方法只写了监听,具体逻辑项目没用到)

import route from '@/utils/routeBlocker'
路由封装-方法链接


无感刷新 token 配置,具体代码如下


import operate from "@/common/operate"import store from'@/store-ts/index'import route from'@/utils/routeBlocker'

/*解决: 类型“string | AnyObject | ArrayBuffer”上不存在属性“code”。
类型“string”上不存在属性“code”。
*/interface Codeable {
data : Object
| String |ArrayBuffer,
code : Number,
}
//请求对列 / 请求状态 let requestQueue =[],
isRefreshing
= false;

export const request
= function(param : any) {//请求参数 let url =param.url,
method
=param.method,
header
={},
data
= param.data ||{},
hideLoading
= param.hideLoading || false;//拼接完整请求地址 let requestUrl = operate.api() +url;//跨域解决 //let requestUrl = url; //console.log(requestUrl) //请求方式:GET或POST if(method) {
method
= method.toUpperCase(); //小写改为大写 if (method == "POST") {
header
={//'content-type': 'application/x-www-form-urlencoded', 'content-type': "application/json",
};
}
else{
header
={'content-type': "application/json",
};
}
}
//拼接header 登录参数 let jointHeader =Object.assign({}, header, operate.commonBeg());//用户交互:加载圈 if (!hideLoading) {
uni.showLoading({
title:
'加载中...',
mask:
true});
}
//请求-拦截器 //requestBlocker() //开始请求 return new Promise((resolve, reject) =>{//判断有无网络验证 noneNetwork().then(() =>{//请求放到promise队列,等待更新token后重新调用。 addRequestQueue(url, method, param.data, jointHeader)//更新 token flushToken().then(() =>{//执行等待的请求 onRefreshed().then(resolve).catch(reject);
})
}).
catch(() =>{//隐藏加载 if (!hideLoading) {
uni.hideLoading();
}
})
//开始请求 uni.request({
url: requestUrl,
data: data,
method: method,
header: jointHeader,
success(res) {
let data
=res.data as Codeable//code判断: 刷新令牌 if (data.code == 401) {//处理token刷新 if (!isRefreshing) {
isRefreshing
= true //请求放到promise队列,等待更新token后重新调用。 addRequestQueue(url, method, param.data, jointHeader)//更新 token flushToken().then(() =>{//执行等待的请求 onRefreshed().then(resolve).catch(reject);
})
}
return;
}
//code判断: 重新登录 if (data.code == 403) {
restartLogin()
return;
}
//将结果抛出 resolve(data)
},
//请求失败 fail: (err) =>{
operate.toast({
title:
'网络连接错误',
icon:
'loading'})//将结果抛出 reject(err)/*.catch(err=>{
console.log(err)
})
*/},//请求完成 complete() {//隐藏加载 if (!hideLoading) {
uni.hideLoading();
}
}
})
})
}
//执行等待的请求 const onRefreshed = () =>{return new Promise((resolve, reject) =>{
let item
=requestQueue.shift();//console.warn('执行等待的请求', item); request({
url: item.url,
method: item.method,
hideLoading:
true,
data: item.data,
}).then(resolve).
catch(reject)
});
}
//添加请求到队列 const addRequestQueue = (url : string, method : object, data : object, header : object) =>{
requestQueue.push({
url,
method,
data,
header
})
}
/**
* @description: 登录刷新 token 请求接口
* @return
*/export const flushToken= function() {return new Promise((resolve, errs) =>{
uni.request({
url: operate.api()
+ '/app-api/refresh-token-刷新接口地址',
method:
'POST',
header: {
'Content-Type': 'application/json'},
data: {
refreshToken: store.getters[
'flushToken'],
},
success(res) {
//console.warn('刷新令牌', res.data); let data =res.data as anyif (data.code == 0) {//登录刷新 store.commit("user/REFRESH_TOKEN", {
accessToken: data.data.accessToken,
refreshToken: data.data.refreshToken
});

resolve(
'刷新令牌成功')return}//登录 失效 if (data.code == 401) {
operate.showModal({
title:
'您的登陆已过期',
confirmText:
'重新登录',
showCancel:
false,
}).then((_res)
=>{//清除登录信息 store.commit('user/LOG_OUT');//去登录页 restartLogin(false)
})
}
},
fail: (err)
=>{
console.error(
'刷新令牌失败', err);
errs(err)
},
complete() {
isRefreshing
= false}
})
})
}
/**
* @description: 请求-拦截器
* @return
* 通过拦截器,实现请求前后的数据监听
*/ //const requestBlocker = function () {//uni.addInterceptor('request', {//invoke(args) {//console.log("求前后的数据监听",args);//}//})//} /**
* @description: 判断有无网络验证
* @return
*/const noneNetwork= function() {return new Promise((resolve, reject) =>{
uni.getNetworkType({
success(res) {
if (res.networkType == 'none') {
uni.showModal({
title:
'没有网络',
content:
'请检查您的网络',
showCancel:
false,
success: (_res)
=>{
resolve(
"无网络确定-返回")
}
});
}
},
complete() {
reject(
'取消-加载圈')
}
})
})
}
/**
* @description: 重新登录(统一方法)
* @return
*/const restartLogin= function (toastShow = true) {if(toastShow) {
operate.toast({
title:
"登录超时!请重新登录"})
}
setTimeout(()
=>{
route({
url:
'/pages/logIn/logIn',
type:
"navigateTo",
login:
false,
})
},
500)
}

requset.ts

缺点:在多个请求同时进行,会出现多次调用(刷新token)的情况。

当前使用
isRefreshing
判断请求刷新转态,没结束不能在调用刷新接口,对于复数请求,还是会多次请求刷新。

下图测试为,同时请求4个接口

项目地址:
https://gitee.com/jielov/uni-app-tabbar

飞书lark机器人 自动化发版

#1 介绍

  • 开发飞书机器人接收消息并调用构建接口, 实现自动化发版

  • 发送指令 -> 机器人接收指令 -> 调用jenkins-job远程构建与部署

  • jenkins配置,勾选job配置的
    触发远程构建
    并设置
    身份验证令牌

#测试 触发远程构建
curl -ks -u user:user_token -X POST \
  jenkins_url/job/job_name/buildWithParameters?token=job_token


#2、创建机器人

#2.1 登录开放平台

飞书
https://open.feishu.cn/
lark
https://open.larksuite.com/

lark是飞书国际版

#2.2 创建应用
  • 创建应用 -> 创建企业自建应用cici ->添加应用能力
    机器人
  • 凭证与基础信息 -> 复制
    App ID

    App Secret
  • 事件与回调 -> 加密策略 -> 复制
    Verification Token
  • 权限管理,添加如下权限:
    • 获取与更新群组信息
    • 以应用的身份发消息
    • 接收群聊中@机器人消息事件
#2.3 运行机器人服务

配置环境变量文件.env_lark

#vim .env_lark 
APP_ID=cli_a7e8508040f99999
APP_SECRET=0iD0HYbmUPrI9aHfHX0NyhL0fy699999
VERIFICATION_TOKEN=vk0SOUPy8MViGxVesPJSAeI5wA799999
ENCRYPT_KEY=""
LARK_HOST=https://open.larksuite.com
#FLASK_ENV=production
JenkinsBaseUrl=https://user:user_token@jenkins.elvin.vip/job/

使用docker启动机器人服务

docker rm -f robot-lark &>/dev/null
docker run -dit --name robot-lark \
 --restart=always -h robot-lark --net=host\
 -v $(pwd):/opt --env-file .env_lark \
registry.aliyuncs.com/elvin/python:lark-robot \
python3 /opt/lark-robot.py

lark-robot.py
实例在https://gitee.com/alivv/elvin-demo

nignx配置域名和lark反向代理

#lark
location ~ ^/(url_verification|lark-cicd) {
    proxy_pass http://127.0.0.1:8092;
    proxy_set_header Host $host:$server_port;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
#2.4 发布应用

2.4.1 配置事件与回调

2.4.2 版本管理与发布,创建版本,申请线上发布


#3 发送消息测试

  • 创建lark群,添加机器人,发送消息测试


#4 源码

python实例如下:

#!/usr/bin/env python3
#lark-robot.py

import os
import logging
import json
import uuid
from flask import Flask, request, jsonify
import lark_oapi as lark
from lark_oapi.api.im.v1 import CreateMessageRequest, CreateMessageRequestBody
from datetime import datetime, timedelta
import requests

# 创建 Flask 应用实例
app = Flask(__name__)

# 配置日志
logger = logging.getLogger('lark-robot')
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

# 存储已经处理过的 request_id
processed_request_ids = set()

# 处理所有请求的前置方法
@app.before_request
def handle_all_requests():
    path = request.path
    if path == '/url_verification' or path == '/lark-cicd':
        return None  # 让请求继续传递给相应的路由处理
    else:
        if 'X-Forwarded-For' in request.headers:
            ip = request.headers['X-Forwarded-For'].split(',')[0]
        else:
            ip = request.remote_addr
        return ip + "\n", 200, [("Server", "Go"), ("City", "Shanghai")]

# URL 验证接口
@app.route('/url_verification', methods=["POST"])
def url_verification():
    req = request.json
    if req.get("token") != VERIFICATION_TOKEN:
        raise Exception("VERIFICATION_TOKEN is invalid")
    return jsonify({"challenge": req.get("challenge")})

# 主业务逻辑接口
@app.route('/lark-cicd', methods=["POST"])
def index():
    req = request.json
    request_id = str(uuid.uuid4())
    # logger.info(f"Received request with ID: {request_id}, data: {req}")
    header = req.get("header", {})
    event_type = header.get("event_type")
    create_time = header.get("create_time")

    if req.get("type") == "url_verification":
        return url_verification()
    elif event_type == "im.message.receive_v1":
        event = req.get("event")
        message = event.get("message")
        group_id = message.get("chat_id")
        msg_content = json.loads(message.get("content")).get("text").split('@')[0]
        msg_content = msg_content.rstrip()

        # 检查 request_id 是否已经被处理过
        if request_id in processed_request_ids:
            logger.info(f"Request ID: {request_id} - Request already processed")
            return "succeed"
        else:
            processed_request_ids.add(request_id)  # 标记 request_id 为已处理
            if create_time: #检查消息是否在10秒以内
                try:
                    create_time_dt = datetime.fromtimestamp(int(create_time) / 1000)  # 转换为datetime对象
                    current_time_dt = datetime.now()
                    if current_time_dt - create_time_dt > timedelta(seconds=10):
                        logger.info(f"Request: {request_id} - {msg_content} - Message is too old")
                        return "succeed"
                except ValueError:
                    logger.error(f"Request ID: {request_id} - Invalid create_time format")
                    return "succeed"
            else:
                logger.error(f"Request ID: {request_id} - Missing create_time")
                return "succeed"

            if msg_content:  # 检查 msg_content 是否为空
                msg_name = next((mention.get("name") for mention in message.get("mentions", []) if mention.get("name")), None)
                logger.info(f"Msg: {msg_content} @{msg_name}")
                response_content = f"已收到 \n{msg_content}"
                # send_event_message(group_id, response_content)
                msg_cicd(group_id, msg_content)
                return "succeed"
            else:
                logger.warning(f"Request ID: {request_id} - message content is empty")
                return "succeed"
    else:
        logger.warning(f"Request ID: {request_id} - Unsupported event type: {event_type}")
        return "succeed"

# 发送消息到群聊
def send_event_message(group_id, response_content):
    client = lark.Client.builder() \
        .app_id(APP_ID) \
        .app_secret(APP_SECRET) \
        .domain(LARK_HOST) \
        .enable_set_token(True) \
        .log_level(lark.LogLevel.ERROR) \
        .build()

    request_body = CreateMessageRequestBody.builder() \
        .receive_id(group_id) \
        .msg_type("text") \
        .content(json.dumps({"text": response_content})) \
        .uuid(os.urandom(16).hex()) \
        .build()

    request = CreateMessageRequest.builder() \
        .receive_id_type("chat_id") \
        .request_body(request_body) \
        .build()

    response = client.im.v1.message.create(request)

    if not response.success():
        lark.logger.error(
            f"client.im.v1.message.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}")
        return

    lark.logger.info(lark.JSON.marshal(response.data, indent=4))
    return "succeed"


##########
#cicd

#筛选消息,执行指令
def msg_cicd(group_id,text):
    msg = text
    #print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "msg->: ",  msg)
    
    #check group
    #test oc_492604c3bb7382afeb47448b726e0a7d
    if group_id != "oc_492604c3bb7382afeb47448b726e0a7d__":
        appInfoMap = dict(appProd, **appProdTest)
        myMenu = {"help", "prod", "test"}
        L = msg.split(" ")
        L = list(filter(lambda x: x != '', L))
        Len = len(L)
        if msg in appInfoMap:
            app_env = appInfoMap[msg][0]
            app_name = appInfoMap[msg][1]
            if msg.startswith("b"):
                app_url = appInfoMap[msg][2] + appInfoMap[msg][0]
            else:
                app_url = appInfoMap[msg][2]
                app_url = app_url + app_env + "&app_list=" + app_name
            if app_env != "":
                #执行通知
                msg = "env:  %s\napp  %s" % (app_env, app_name)
                send_event_message(group_id, msg)
                
                #向webhook发起post请求
                head = { 'User-Agent': "webhook-robot" }
                res = requests.post(url=app_url, headers=head)
                print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "webhook", app_env, app_name, res.reason)
                return "succeed"
            else:
                print(msg, "nothing")
                return "succeed"
        elif msg in myMenu:
            #打印命令列表
            print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "Send menu")
            msgTitle = "#命令  名称\n"
            if msg == "help":
                msgTitle2 = "#命令  获取列表\n"
                msg = msgTitle2 + "prod  app-prod-list\ntest  app-test-list"
            elif msg == "prod":
                msg = msgTitle
                for i in appProd:
                    msg = msg + i + "  " + appInfoMap[i][1] + "\n"
            elif msg == "test":
                msg = msgTitle
                for i in appProdTest:
                    msg = msg + i + "  " + appInfoMap[i][1] + "\n"
            msg = msg.rstrip('\n')
            send_event_message(group_id, msg)
            return "succeed"
        #多个app部署
        elif Len > 1:
            app = ""
            apps = ""
            app_env = ""
            for n in L:
                if n in appInfoMap:
                    app_name = appInfoMap[n][1]
                    app = app + app_name + " \n"
                    apps = apps + app_name + " "
                    app_env = appInfoMap[n][0]
                    app_url = appInfoMap[n][2]
            if app_env != "":
                #执行通知
                app = app.rstrip('\n')
                msg = f"env:  {app_env}\napp-list: \n{app}"
                send_event_message(group_id, msg)
                
                #向webhook发起post请求
                app_url = app_url + app_env + "&app_list=" + app
                head = { 'User-Agent': "webhook-robot" }
                res = requests.post(url=app_url, headers=head)
                print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "webhook", app_env, apps, res.reason)
                return "succeed"
            else:
                msg = f"已收到: \n{msg} \n发送 help@cici 查看支持指令"
                send_event_message(group_id, msg)
                return "succeed"
        else:
            msg = f"已收到: \n{msg} \n发送 help@cici 查看支持指令"
            send_event_message(group_id, msg)
            return "succeed"

    else:
        print("group_id no found",group_id)
        return "succeed"


# 从环境变量加载配置
APP_ID = os.getenv("APP_ID")
APP_SECRET = os.getenv("APP_SECRET")
VERIFICATION_TOKEN = os.getenv("VERIFICATION_TOKEN")
LARK_HOST = os.getenv("LARK_HOST", "https://open.larksuite.com")

##########
#cicd list

#webhook url for jenkins 
JenkinsBaseUrl = os.getenv("JenkinsBaseUrl")

#job
appDeploy = "test-app-deploy/buildWithParameters?token=cicdTest&app_branch=master&app_build=true&docker_build=true&create_git_tag=false&notice_msg=true&app_deploy=true&image_update=true&input_pass=true&deploy_tag=tag&deploy_env="

#ci url
appDeployUrl = JenkinsBaseUrl + appDeploy

appProd = {
"#app-prod-k8s-list:": ["","", ""],
"s101": ["prod","app-web", appDeployUrl],
"s102": ["prod","app-svc", appDeployUrl],
"s103": ["prod","app-api", appDeployUrl],
"s104": ["prod","app-event", appDeployUrl],
"s105": ["prod","app-admin", appDeployUrl],
}

appProdTest = {
"#app-test-k8s-list:": ["","", ""],
"s201": ["test","app-web", appDeployUrl],
"s202": ["test","app-svc", appDeployUrl],
"s203": ["test","app-api", appDeployUrl],
"s204": ["test","app-event", appDeployUrl],
"s205": ["test","app-admin", appDeployUrl],
}

##########

# 启动 Flask 应用
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8092, debug=False)

source:
https://gitee.com/alivv/elvin-demo

在使用 wxPython 开发跨平台桌面应用程序时,创建一个
图标选择界面
通常用于让用户从图标资源库中选择图标,我们可以把图标分为自定义的图标资源和系统的图标资源两大类,最终我们把它们整合一起使用,在框架的界面中使用,包括工具栏、右键菜单、按钮、图片等所需的地方显示,实现图文并茂的友好界面展示。本篇随笔介绍这两种图标资源的管理和使用过程。

1、图标分类介绍

这里图标我们分为两类,一类是wxPython内置的图标资源,以wx.Art_开始。
wx.ART_
是 wxPython 提供的艺术资源(Art Resource)常量,用于在界面中快速访问通用的图标或位图资源。这些资源可以通过
wx.ArtProvider
获取,主要用于创建标准化的界面,例如工具栏、菜单和对话框中的图标。这类可以使用
wx.ArtProvider.GetBitmap

wx.ArtProvider.GetIcon
获取对应的位图或图标。

bitmap = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_OTHER, (32, 32))
icon
= wx.ArtProvider.GetIcon(wx.ART_WARNING, wx.ART_OTHER, (16, 16))

参数说明:

  • id
    :指定图标的类型,例如
    wx.ART_INFORMATION

    wx.ART_WARNING
    等。
  • client
    :指定图标的用途,一般用
    wx.ART_OTHER
  • size
    :指定图标的尺寸,例如
    (16, 16)

    (32, 32)

常用
wx.ART_
图标列表

以下是一些常见的
wx.ART_
常量及其用途:

常量名称 描述 示例用途
wx.ART_INFORMATION 信息图标(蓝色圆圈中的“i”) 消息框或工具栏中的信息提示
wx.ART_WARNING 警告图标(黄色三角形) 警告对话框
wx.ART_ERROR 错误图标(红色圆圈中的“X”) 错误提示框
wx.ART_QUESTION 问号图标 确认对话框中的问题提示
wx.ART_FILE_OPEN 打开文件图标 文件打开按钮或菜单项
wx.ART_FILE_SAVE 保存文件图标 文件保存按钮或菜单项
wx.ART_FILE_SAVE_AS 另存为图标 另存为操作
wx.ART_DELETE 删除图标 删除按钮或菜单项
wx.ART_COPY 复制图标 工具栏中的复制按钮
wx.ART_CUT 剪切图标 工具栏中的剪切按钮
wx.ART_PASTE 粘贴图标 工具栏中的粘贴按钮
wx.ART_UNDO 撤销图标 撤销操作按钮
wx.ART_REDO 重做图标 重做操作按钮
wx.ART_FIND 查找图标 查找工具栏或菜单项
wx.ART_HELP 帮助图标(问号) 帮助按钮或菜单项
wx.ART_NEW 新建文件图标 新建按钮或菜单项
wx.ART_TIP 提示图标 工具提示或信息提示框
wx.ART_ADD_BOOKMARK 添加书签图标 书签管理工具
wx.ART_DEL_BOOKMARK 删除书签图标 书签管理工具
wx.ART_GO_BACK 返回图标 浏览器中的后退按钮
wx.ART_GO_FORWARD 前进图标 浏览器中的前进按钮
wx.ART_GO_UP 向上导航图标 文件管理器中的向上导航
wx.ART_GO_DOWN 向下导航图标 文件管理器中的向下导航
wx.ART_EXECUTABLE_FILE 可执行文件图标 显示可执行文件类型
wx.ART_NORMAL_FILE 普通文件图标 显示普通文件类型
wx.ART_FOLDER 文件夹图标 显示文件夹类型
wx.ART_FOLDER_OPEN 打开文件夹图标 文件夹展开时显示
wx.ART_PRINT 打印图标 打印操作按钮或菜单项
wx.ART_REPORT_VIEW 报表视图图标 改变视图样式的按钮
wx.ART_LIST_VIEW 列表视图图标 改变视图样式的按钮

通过使用
wx.ART_
,可以显著提高界面的一致性和开发效率!

另外一类是我们自己自定义的图标资源,为了方便整合使用,我们往往把它作为嵌入资源的方式,放在一个文件里面,如下所示。

需要的时候,直接导入这个core_image进行使用即可。

importcore.core_images as images

img: wx.Image
=(
images.catalog[name].Image
if name in images.catalog elseresult
)
img
=img.Scale(icon_size, icon_size, wx.IMAGE_QUALITY_HIGH)
result
= wx.Bitmap(img)

为了方便添加,我们可以使用wxpython的内置工具类img2py来生成代码到指定.py文件中。

from wx.tools import img2py

然后可以通过命令行来生成代码到python类文件中。

"-a -F -n TestStar2 bmp_source/teststar.png images.py"

需要详细了解各个参数的意思,可以查看 img2py.py 例子代码的说明即可。

我为了方便,参考修改编写了一个独立的工具类来方便生成所需的代码。

我可以再文件目录中选择所需的文件,然后右键复制文件地址,把它们路径复制出来。

2、图标的选择界面及使用

前面介绍了两种不同的图标资源库,我们应该再框架系统中能够顺利使用它们,以便为我们的工具栏、菜单、按钮等地方添加有趣而有意义的图标,比单纯的文本会好看很多。

如列表界面中的工具栏、按钮都采用了和内容一致的图标,比较美观一些。

有时候,我们右键菜单也可以根据图表库选择一致意义的图标。

表格显示的右键菜单

或者树形列表的右键菜单

因此我们在系统中统一展示和选择图标操作,如下所示,包括自定义图标,也包括内置图标。

自定义图标可以根据需要进行添加到内置资源的类即可,图标来源很多,网上也有很多,可以参考一下下面的这个网址获取一些:
https://www.iconarchive.com/

在里面搜索关键字下载自己所需图标即可。

为了统一通过名称获取图标信息,我们可以编写一个通用的处理图标的函数来处理,如下函数。

def get_bitmap(name: str, icon_size: int = 32) ->wx.Bitmap:"""获取位图,可以根据images里面的内容,或者是内置的wx.ART_XXX"""result=wx.ArtProvider.GetBitmap(
wx.ART_FOLDER, wx.ART_OTHER, (icon_size, icon_size)
)
try:if name is None or name == "":returnresultif name.startswith("wx.ART_"):
result
=wx.ArtProvider.GetBitmap(
eval(name), wx.ART_OTHER, (icon_size, icon_size)
)
else:
img: wx.Image
=(
images.catalog[name].Image
if name in images.catalog elseresult
)
img
=img.Scale(icon_size, icon_size, wx.IMAGE_QUALITY_HIGH)
result
=wx.Bitmap(img)exceptException as e:print(f"get_bitmap 调用错误: {e},没有找到{name}的图片")return result

这样我们在创建按钮、菜单的时候,使用这个生成图标位图的辅助函数即可通用了。

        button =wx.Button(parent, id, btn_name)
button.Enable(enable)
ificon_name:
button.SetBitmap(get_bitmap(icon_name, icon_size))
button.SetBitmapPosition(wx.LEFT)
#可以设置 LEFT, RIGHT, TOP, BOTTOM

前言

今年,是我有生以来,个人生活体验最为丰富的一年,也是能量值极其饱满的一年。
年初,我在flomo记录了一条笔记(立了一个flag):2024年,我要做一个元气满满的人。
我很开心,这个flag,我实现了,而且做得非常好!
这一年,我过得非常充实!非常精彩!非常开心!
在29岁的这一年,我成为了自己最喜欢的那一类人,我感到特别幸福!

今年,我找到了一份很喜欢的新工作

今年,我换工作了。
第一次开始写这样的个人年终总结,是在2021年入职上家公司的时候。
刚入职的时候,前东家有个大项目在做,团队氛围也比较好,那时的我,对这家公司的未来也充满了期待。
然而,谁也想不到,2022年,一切都变了:原先的大项目即将收尾,因为某些原因,也没有新的长期项目可以做了,于是就开始零零散散的做一些小项目。
比起没有稳定的项目,更为难受的是,开发的功能需求不明确,开发出来的系统没人用,导致大家都缺乏成就感,越来越疲惫,原先团队的良好氛围也没了,离职的人越来越多。
因为外部环境不好,加上当时也还没想清楚自己的下一份工作想找什么类型的,所以一直到2023年,我都还没提离职,就在那里耗着。
今年年初,我得了一场严重的风热感冒,也休了好几天的假。
在生病期间,我沉下心来思考了许多东西,也想明白了一些关键问题。
在生病之前,我本来在备考PMP证书。因为那场感冒特别严重,退烧之后,还连续咳嗽了好几个星期,状态特别差,思考一番之后,我决定暂缓考证计划。
一来,当时状态不好,影响备考状态,万一考不好,还影响自己心态。
二来,证书只是个锦上添花的东西,而当下更为重要的,很明显是工作方面的问题,已经逃避了那么久,也是时候面对这个问题了。
也就是在那段时间里,我清楚的意识到,继续留在原来那家公司,只会持续不断的消耗我的心力,让我逐渐失去曾经的朝气与活力。
之后没多久,我就下定决心,过完年回来,不论如何,我都要提离职。
下定决心之后,我也就开始思考最关键的一个问题:下一份工作,我想要找什么类型的?
我回顾了自己以往的工作经历,发现除了上一份工作,我的其它工作经历,都是跟制造业相关的。
我似乎也挺适合去开发制造业相关的系统,这类系统,往往对于技术并没有太高要求,但是很重视业务流程。而梳理清楚业务逻辑、输出业务流程图和业务说明文档,刚好都是我比较擅长的事情,也是我比较喜欢做的事情。
熟悉了制造业的业务知识,将来就算不做开发了,转型去做业务专家、去做方案专家或者是其他管理岗位,也都是有机会的。
况且,制造业也是国家大力支持、很有发展前景的行业。
所以,我希望自己的下一份工作,要尽量往制造业方向靠拢。
最终,关于这个问题,我得出的结论大致如下:
1、最优选项:找一份开发制造业信息管理系统相关的新工作,并且所在团队要很有发展前景,也能够给出比较有竞争力的薪资待遇。
2、普通选项:如果实在找不到制造业的岗位,也可以继续在互联网行业找,但不要是外包公司,可以是有发展潜力的初创公司,也可以是较为成熟的中大型公司,关键是要有稳定的项目或产品可以做。
3、最差选项:如果以上两种都找不到,我允许自己有几个月的职场空窗期,一边找工作,一边修养身心,但是一旦超过一定时间范围,就不要挑了,外包什么的也行,先有份工作再说。
很幸运,最终,我的新工作基本是符合最优选项的。
新公司在制造业已经深耕多年,实力强大。
不过信息化程度比较低,研发团队属于新团队,需要从零到一搭建自己公司的生产制造全流程相关的系统。
对于我来说,这既是挑战,也是非常大的机遇。
到现在,入职已经大半年了,我们团队研发的系统也在越来越多的部门使用起来了,我在业务知识方面也有了不少沉淀,可以说,这份工作,完美符合我的预期,是我自大学毕业以来,最喜欢的一份工作。
当然,不存在完美的公司,每家公司都有好有坏,我们公司也一样。
不过,每个人都有自己的取舍,对于我而言,我在工作方面所想要获得的成长和资源,目前这家公司都能够提供给我。至于其他东西,也都在我可接受范围内。
这就足够了,对我而言,这就是一份非常棒的工作。
说来也巧,如果不是年初刚好大病了一场,推迟了考证计划,那我大概率是找不到现在这份新工作的,也算是因祸得福吧。
哈哈哈哈!

今年,我的生活过得非常精彩

受成长环境的影响,从小到大,我一直都过得非常压抑,也没有什么丰富的生活体验。
从2021年开始,我就开始学习如何更好地去生活,如何通过适当的消费来给自己创造更加丰富的生活体验。
不过那两年受疫情影响,所以能做的尝试非常有限。
到了2023年,我在这方面有了比较大的突破:
在工作闲暇之余,报名学习了心理学课程,参加了几次线下课,挺有意思的。
每天晚上准点下班,回家做饭吃,然后出门散步半小时到一小时。
碎片化时间,找了许多历史学视频来看,培养了新的兴趣爱好。
......
只是,2023年,在工作方面,始终有些迷茫,因而有许多原本想做的事情,最终还是攒足相应的力量去执行。
而今年,我真的毫无遗憾,因为过去多年我所想要尝试的事情,今年,基本都实现了。
今年过完年回深圳后当天晚上,我记录了一个想法:
今年应该会尝试许多新的生活方式。

如同自己所说一样,有什么喜欢做的事情,就去做,去尽情体验。

好比此刻,因为实在是太想听这首「大梦」了,所以今晚肯定会比平时晚睡一点,但是没关系,好好体验一下沉浸在音乐之中的瞬间。

这种如此喜欢一样东西的感觉,也不是常有的,当它出现的时候,就好好感受一下。

这种感觉,挺纯粹的。
当时跟一位好友分享了这个想法,他对我说:
去旅行,真的。
旅游就是花钱还能量换体验。
对于年轻人而言,是绝佳投资。
然后,我真的按照自己的想法去度过了这一年,也听取了朋友的建议,实现了人生的第一次跨省旅游。
这一年:
我跟朋友一起去看了人生第一场音乐会,在音乐厅里面听音乐会的体验很棒,跟手机或电脑放歌的感觉完全不一样。

我还去了虹桥公园、深圳人才公园、深圳铁仔山公园,去爬了阳台山......
以前没去过的许多景点,今年都去逛了一遍。

参加了朋友的婚礼,也是人生第一次参加朋友的婚礼。很喜欢他们的婚礼,看完之后很心动,也想办一场属于自己的婚礼。
先找个女朋友吧,哈哈哈哈。

实现了人生的第一次跨省旅游:苏州及无锡之旅。
去苏州逛了西园寺、寒山寺、苏州博物馆、东方之门、金鸡湖、拙政园、狮子林、平江路。
去无锡逛了南长街。
太喜欢苏州这座城市了,人生的第一次跨省旅游,体验感拉满,特别开心!
这只是个开始,以后,我还会去更多不一样的地方旅游。

当然,工作也好,生活也罢,总会有烦躁的时候,总会有不顺利的时候,但相比起以往,
我已经能够靠自己的力量去走出情绪低落期,能够靠自己的力量去刷新情绪能量。
总之,今年,不管是工作还是生活,我都过得非常自在,都是在按照自己的方式去体验,没留下什么遗憾。

今年,我最喜欢的书

今年,我最喜欢的一本书是史铁生的《我与地坛》。

书摘和读书笔记

我摘录了一些我非常喜欢的片段,也做了一些读书笔记:
摘录1:
我一连几小时专心致志地想关于死的事,也以同样的耐心和方式想过我为什么要出生。
这样想了好几年,最后事情终于弄明白了:一个人,出生了,这就不再是一个可以辩论的问题,而只是上帝交给他的一个事实;
上帝在交给我们这件事实的时候,已经顺便保证了它的结果,所以死是一件不必急于求成的事,死是一个必然会降临的节日。
这样想过之后我安心多了,眼前的一切不再那么可怕。

——第1章 我与地坛

我的笔记1:
对于不可辩论、无法改变之事,我们能做的,就只有接受、理解并坦然面对。
在此基础上,好好想一下,在这有限的生命中,我们还能做些什么,该怎么去生活?
而这,不是一个能够一次性回答清楚的问题,恐怕是活多久,就要想多久。
摘录2:
此文开头说的那位二十一岁的朋友——我们知青的第二代,他喜欢唱什么歌呢?有机会我要问问他。
但是他愿意唱什么就让他唱什么吧,世上的一些事多是出于瞎操心,由瞎操心再演变为穷干涉。
我们的第二代既然也快到了恋爱的季节,我们尤其要注意:任何以自己的观念干涉别人爱情的行为,都只是一股逆流。

——第6章 黄土地情歌
我的笔记2:
每一代人都有自己的路要走,上一代人可以将自己的人生阅历分享、传授给下一代人,但不必去干涉他们的选择。
世界一直在变化,曾经的真理可能是如今的谬误。
上一代人所笃定的真理,不一定适合下一代人。
要相信,当代人会比上一代人更加懂得,什么才是适合他们的。
摘录3:
过程——只剩了它了。

事实上你惟一具有的就是过程。

一个只想(只想!)使过程精彩的人是无法被剥夺的,因为死神也无法将一个精彩的过程变成不精彩的过程,
因为坏运也无法阻挡你去创造一个精彩的过程,相反你可以把死亡也变成一个精彩的过程,相反坏运更利于你去创造精彩的过程。

——第8章 好运设计

我的笔记3:
如果执着于追求目的,不论结果如何,都会因为新的目的而无法享受到完整的美好体验,只要陷入了对于目的的追求,就无法战胜命运所安排的失败和绝境。
而过程则不一样,过程本身就有意义,注重过程的体验,就是将生命价值最大化的一种方式。
不论你的终局是什么,只要你能够享受这个过程,只要你创造了一个精彩的过程,那么即便是死神也无法改变这一事实,即便是死神也无法夺走你的美好回忆。
而这些美好的回忆,终将成为你的整个人生乃至生命最后一刻最宝贵的事物。

感悟

小时候读语文课本上节选的《我与地坛》时,都没有读懂,现在仔细阅读原著,着实被史铁生文字所打动。
一个二十岁出头就双腿残疾,之后数十年又一直被各种病痛所折磨的人,其文字却表达出了比无数正常人还要顽强的生命力。
难怪余华会一直对史铁生这位老朋友念念不忘,正如余华所说:
「铁生是我见过的作家里面最乐观的一个人,虽然他的那个身体状况是我认识的作家朋友里面最差的一个,但他一直是一个最乐观的人。」
「铁生是一个对这个世界没有任何那种恶意的一个人,而且怨言都没有,完全没有,对这个世界充满了爱。」
「他把他当年那种艰苦的生活,当他在重新回顾的时候,他发现他的青春岁月,他的生命中最美好的一段岁月,他把那种美好写出来了,那是典型的铁生的风格。」
他们那一代人如果要选择代表人物的话,史铁生必然名列其中。
倘若有来生,真心希望史铁生能拥有一个健康的体魄和灵魂,过上他所渴望的生活!

种一棵树最好的时间是十年前,其次是现在

年初在网上看到这样一段话:
多经历,才能看懂书。

只有你经历过一些事了,才能懂一些道理。

没经历就是理解不了,讲是很难讲懂的,要相信时间,过两年再看看。

上面这个道理也得经历过了“经历了一些事儿”——“突然发现一个自己熟背的道理是什么滋味”的过程才能懂。

可以说是“元道理”了。
随后,我结合自身的一些经历,写了这样一条笔记:
大学期间曾看过一段话:种一棵树最好的时间是十年前,其次是现在。

当时觉得这段话很好理解,认为自己今后肯定可以贯彻这段话的思想。

但是,回顾自己前几年的生活经历:

貌似一直在努力学习,但却总是“浅尝辄止”。

每次想学点比较具有挑战性但很有用的东西时,总会给自己设限:我已经错过了最好的学习时间点了,现在学习这个已经很难追赶上别人了。

到了第二年,又经常反思:当时要是直接去做就好了,干嘛非得跟别人比,自己有进步就行了。

令人哭笑不得的是,反思完,新的一年,依旧没有去行动。就这样,荒废了许多光阴。


近几个月,也许是对这句话有了更深刻的理解,心态方面有了比较大的转变。

现在想要做什么事情,会先思考一下,觉得值得做的话就直接去做了,不再给自己设限。

然后每隔一段时间会总结自己做了哪些事情,取得了哪些进步,这样也能够获得很好的正反馈。

或许,先前的经历,也是我想要明白这个道理所“必须经历的事情”。
以上,也算是近几年,我在认知方面所获得的一个重大突破了。
仔细一想,今年,我的许多进步,都与之相关。

工作方面

决定要换工作了,就马上分析自己的工作经历以及性格特点,想明白自己的下一份工作想要找什么类型的,然后马上准备简历以及刷面试题。
找工作不顺利时,也没有想过要放弃,也没有考虑过要留在原来的公司,而是继续优化简历,增加投简历的渠道,并且经常给自己加油打气。
以前学东西总是学着学着就学不下去了,因为不管学什么,总是会冒出和以前相类似的想法:我现在学这个,会不会有点晚了?我现在学这个,将来真的能派上用场吗?
今年,我想明白了,至少最近几年,专注于制造业,开发好相关的业务系统,积累丰富的行业经验,就是我想要学习的东西。
想清楚以后,如今不管是在公司还是在家,都会主动去学习与制造业相关的业务知识,并且完全没有以往那些杂念。

生活方面

想去听音乐会时,尽管路途稍微有点远,也照样去了,结果也获得了比较好的体验。
想去逛公园了,就立马出门,而不是找借口说:等忙完这一段时间再说。
想去旅游了,就提前把工作处理好,然后跟领导请好假,直接就出门了,也确实获得了前所未有的美好体验。
总之,放下"等什么什么时候就好了"以及"等什么什么之后再去干嘛"之类的想法,就可以过得很轻松,想要做什么,就马上开始做,不必犹豫。
随时随地,尽情享受当下的生活。

结语:如果人生只是大梦一场

十年前,我刚踏入大学校园的时候,我告诉自己:我绝不会再像中学时期那般荒废青春,我要好好把握这四年的大学时光,好好学习,成为一个很强大的人!
大学期间,我也一直很努力在朝着这个目标前进。尽管那时的我,因为无法认识到自身的许多问题,在学习、与人相处以及生活方面都存在诸多问题,也过得相辛苦,但好在,我一直在坚持,也结识了一批非常要好的朋友,受到了许多人的帮助。
十年前,我对于未来,感到相当迷茫。我很希望未来的自己是积极向上、内心强大的,我希望未来的自己可以从容不迫地应对生活中的各种问题,能够不再那么焦虑,能够很好的享受生活。
倘若十年前的我,能够看到我现在的生活状况,想必,他会感到十分开心吧!
在29岁这一年,我过上了自己所喜欢的生活,我找到了一份自己很喜欢的新工作,我体验了许多以前从未体验过的事情,我成为了一个非常热爱生活的人,我成为了自己最喜欢的那一类人。
正因为曾经的我,一直过得非常辛苦、非常压抑,在性格方面也存在许多问题,我才更加清楚,自己能够走到今天这一步,除了自身的努力因素,也离不开运气的加持,更不离开生命当中那些朋友、贵人们的帮助。
所以,我觉得自己非常幸运!非常幸福!
最后,就用下面这段话,来为我的2024画上一个圆满的句号:
如果人生只是大梦一场,那么我能把握的,就只有这个过程。
我要尽情体验自己的人生,我要做个有自我意识、有血有肉的人,我要去做我喜欢做的事情,不管简单的还是困难的,我都想去做,并且要一路走到底。
至此,我对未来,依然充满期待!