了迹奇有没 2023-08-02T09:09:40+08:00 https://whrss.com/ whrss 更换博客图床,从OSS到GitHub再到R2 pic-archive 2023-08-02T09:09:40+08:00 2023-08-02T09:09:40+08:00

写博客一个很重要的东西就是图床, 如果没有一个合适的图床来安全稳定地存储数据,指不定哪天,你的文章就到处都是

真的找不到了.png

而对于一个曾经被ban过号,崩过盘(硬盘)的人来说,分外敏感。如果你的图片存储在任何一个带监管的平台,你的账号很可能由于一些不可抗力资源牵连,内容全部404。

于是我对自己的博客相关的资源都进行了“安全”的部署,图片资源这块儿一年多来,我一直使用的是github做图床的,但是github有几个问题:

  • 一个仓库的存储大小限制1G,如果超过限制,很可能会被ban库的。

  • GitHub 阻止大小超过 100 MB 的文件。

  • GitHub 在国内网络下无法访问。

于是,几个月前把云资源全部迁到cloudflare的我,又有了白嫖的念头。

能不能使用R2来做图床呢?之前用过阿里OSS做图床的我,本能地想到,一定是可以的!

简单搜搜,R2图床~

不出所料~哈哈哈哈简简单单

当然前提是你需要有一个cloud flare 账号哦

一. cloud flare操作

1. 到cloud flare创建一个bucket

image.png

2. 对bucket进行配置

image.png

3. 然后到r2的列表菜单,申请一个secret

image.png

image.png

二. 本地操作

使用pic-go进行上传,如果没有,请安装一个

1. 在设置中的默认图床不包含cloud flare ,需要安装一下插件,这里的插件需要安装的是aws的S3插件,该插件兼容国外同S3类型的这些所有的对象存储。

需要安装这两个插件,一个是功能,一个是展示。

image.png

然后在设置处就会出来新的选项

image.png

2. 填写下刚才获取到的那些参数

image.png

最后效果如下:


![image.png](https://pic.whrss.com/2023/08/1690961579898.png)

完美~

]]>
写博客一个很重要的东西就是图床, 如果没有一个合适的图床来安全稳定地存储数据,指不定哪天,你的文章就到处都是 ![真的找不到了.png](https://pic.whrss.com/1690960055739.png) 而对于一个曾经被ban过号,崩过盘(硬盘)的人来说,分外敏感。如果你的图片存储在任何一个带监管的平台,你的账号很可能由于一些不可抗力资源牵连,内容全部404。 于是我对自己的博客相关的资源都进行了“安全”的部署,图片资源这块儿一年多来,我一直使用的是github做图床的,但是github有几个问题...
如何在go中实现程序的优雅退出,go-kratos源码解析 go-grace-stop 2023-07-29T05:58:45+08:00 2023-07-29T05:58:45+08:00

使用kratos这个框架有近一年了,最近了解了一下kratos关于程序优雅退出的具体实现。

这部分逻辑在app.go文件中,在main中,找到app.Run方法,点进入就可以了

它包含以下几个部分:

  1. App结构体:包含应用程序的配置选项和运行时状态。

  2. New函数:创建一个App实例。

  3. Run方法:启动应用程序。主要步骤包括:

  • 构建ServiceInstance注册实例

  • 启动Server

  • 注册实例到服务发现

  • 监听停止信号

  1. Stop方法:优雅停止应用程序。主要步骤包括:
  • 从服务发现中注销实例

  • 取消应用程序上下文

  • 停止Server

  1. buildInstance方法:构建用于服务发现注册的实例。

  2. NewContext和FromContext函数:给Context添加AppInfo,便于后续从Context获取。

核心的逻辑流程是:

  1. 创建App实例

  2. 在App.Run()里面启动Server,注册实例,监听信号

  3. 接收到停止信号后会调用App.Stop()停止应用

我们先对Run方法进行一个源码进行查看


// Run executes all OnStart hooks registered with the application's Lifecycle.

func (a *App) Run() error {



  // 构建服务发现注册实例

  instance, err := a.buildInstance() 

  if err != nil {

    return err

  }



  // 保存实例  

  a.mu.Lock()

  a.instance = instance

  a.mu.Unlock()



  // 创建错误组

  eg, ctx := errgroup.WithContext(NewContext(a.ctx, a))



  // 等待组,用于等待Server启动完成

  wg := sync.WaitGroup{}



  // 启动每个Server

  for _, srv := range a.opts.servers {

    srv := srv 

    eg.Go(func() error {

      // 等待停止信号

      <-ctx.Done()  

      // 停止Server

      stopCtx, cancel := context.WithTimeout(a.opts.ctx, a.opts.stopTimeout)

      defer cancel()

      return srv.Stop(stopCtx)

    })



    wg.Add(1)

    eg.Go(func() error {

      // Server启动完成

      wg.Done() 

      // 启动Server  

      return srv.Start(NewContext(a.opts.ctx, a)) 

    })

  }



  // 等待所有Server启动完成

  wg.Wait()



  // 注册服务实例

  if a.opts.registrar != nil {

    rctx, rcancel := context.WithTimeout(ctx, a.opts.registrarTimeout)

    defer rcancel()

    if err := a.opts.registrar.Register(rctx, instance); err != nil {

      return err

    }

  }

  

  // 监听停止信号

  c := make(chan os.Signal, 1)

  signal.Notify(c, a.opts.sigs...)

  eg.Go(func() error {

    select {

    case <-ctx.Done():

      return nil

    case <-c:

      // 收到停止信号,停止应用------------- ⬅️注意此时

      return a.Stop() 

    }

  })



  // 等待错误组执行完成

  if err := eg.Wait(); err != nil && !errors.Is(err, context.Canceled) {

    return err

  }



  return nil

}

核心逻辑就是这里⬇️,使用signal.Notify去监听操作系统给出的停止信号。


  // 监听停止信号

  c := make(chan os.Signal, 1)

  signal.Notify(c, a.opts.sigs...)

  eg.Go(func() error {

    select {

    case <-ctx.Done():

      return nil

    case <-c:

      // 收到停止信号,停止应用

      return a.Stop() 

    }

  })

然后调用了Stop方法,我们再看下Stop的源码


// Stop gracefully stops the application.

func (a *App) Stop() error {



  // 获取服务实例 

  a.mu.Lock()

  instance := a.instance

  a.mu.Unlock()



  // 从服务发现注销实例

  if a.opts.registrar != nil && instance != nil {

    ctx, cancel := context.WithTimeout(NewContext(a.ctx, a), a.opts.registrarTimeout)

    defer cancel()

    if err := a.opts.registrar.Deregister(ctx, instance); err != nil {

      return err

    }

  }



  // 取消应用上下文

  if a.cancel != nil {

    a.cancel() 

  }



  return nil

}

主要步骤是:

  1. 获取已经保存的服务实例

  2. 如果配置了服务发现,则从服务发现中注销该实例

  3. 取消应用上下文来通知应用停止

在Run方法中,我们通过context.WithCancel创建的可取消的上下文Context,在这里通过调用cancel函数来取消该上下文,以通知应用停止。

取消上下文会导致在Run方法中启动的协程全部退出,从而优雅停止应用。

所以Stop方法比较简单,关键是利用了Context来控制应用生命周期。

我们可以注意到,在Run方法中,我们使用到了一个signal包下的Notify方法来对操作系统的关闭事件进行监听,这个是我们动作的核心,我把这部分单独整理在了另一篇文章中。

通过对操作系统事件的监听,我们就可以对一些必须完成的任务进行优雅地停止,如果有一些任务必须完成,我们可以在任务开始使用 wg := sync.WaitGroup{} 来对任务进行一个Add操作,当所有任务完成,监听到操作系统的关闭动作,我们需要使用wg.wait() 等待任务完成再进行退出。以实现一个优雅地启停。

]]>
使用kratos这个框架有近一年了,最近了解了一下kratos关于程序优雅退出的具体实现。
os.signal golang中的信号处理 go-signal 2023-07-29T05:56:55+08:00 2023-07-29T05:56:55+08:00

在程序进行重启等操作时,我们需要让程序完成一些重要的任务之后,优雅地退出,Golang为我们提供了signal包,实现信号处理机制,允许Go 程序与传入的信号进行交互。

Go语言标准库中signal包的核心功能主要包含以下几个方面:

1. signal处理的全局状态管理

通过handlers结构体跟踪每个signal的处理状态,包含信号与channel的映射关系,以及每个信号的引用计数。

2. 信号处理的注册与注销

Notify函数用于向指定的channel注册信号处理,会更新handlers的状态。

Stop函数用于注销指定channel的信号处理,将其从handlers中移除。

Reset函数用于重置指定信号的处理为默认行为。

3. 信号的抓取与分发

process函数在收到signal时,会把它分发给所有注册了该信号的channel。

4. signal处理的恢复

通过cancel函数,可以恢复signal的默认行为或忽略。

5. Context信号通知支持

NotifyContext函数会创建一个Context,在Context结束时自动注销signal处理。

6. 处理signal并发访问的同步

通过handlers的锁保证对全局状态的线程安全访问。

7. 一些工具函数

如handler的mask操作,判断signal是否在ignore列表中等。

总的来说,该实现通过handlers跟踪signal与channel的关系,在收到signal时分发给感兴趣的channel,提供了flexible和高效的signal处理机制。

在实际地使用中,我们需要创建一个接收信号量的channel,使用Notify将这个channel注册进去,当信号发生时,channel就可以接收到信号,后续的业务就可以针对性地进行处理。如下:


package main



import (

"fmt"

"os"

"os/signal"

"syscall"

)



func main() {



// 创建一个channel来接收SIGINT信号

c := make(chan os.Signal)



// 监听SIGINT信号并发送到c

signal.Notify(c, syscall.SIGINT)



// 使用一个无限循环来响应SIGINT信号

for {

fmt.Println("Waiting for SIGINT")

<-c

fmt.Println("Got SIGINT. Breaking...")

break

}

}

共有32个信号量,相对应的枚举在syscall包下

常用的信号值包括:

SIGHUP 1 终端控制进程结束(终端连接断开)

SIGINT 2 用户发送INTR字符(Ctrl+C)触发

SIGQUIT 3 用户发送OUIT字符(Ctrl+/触发

SIGKILL 9 无条件结束程序(不能被捕获、阻塞或忽略)

SIGUSR1 10 用户保留

SIGUSR2 12 用户保留

SIGPIPE 13 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)

SIGALRM 14 时钟定时信号

SIGTERM 15 结束程序(可以被捕获、阻塞或忽略)

在go框架中,项目中实际使用到signal进行优雅退出见:如何在go中实现程序的优雅退出,go-kratos源码解析

]]>
在程序进行重启等操作时,我们需要让程序完成一些重要的任务之后,优雅地退出,Golang为我们提供了signal包,实现信号处理机制,允许Go 程序与传入的信号进行交互。
探索PlanetScale:划分分支的MySQL Serverless平台 planet-scale 2023-07-12T03:37:52+08:00 2023-07-12T03:37:52+08:00

最近我发现了一个非常有趣的国外MySQL Serverless平台,它叫做PlanetScale。这个平台不仅仅是一个数据库,它能像代码一样轻松地创建开发和测试环境。你可以从主库中拉出一个与之完全相同结构的development或staging数据库,并在这个环境中进行开发和测试。所有的数据都是隔离的,不会相互干扰。

当你完成开发后,你可以创建一个deploy request,PlanetScale会自动比对并生成Schema diff,然后你可以仔细审查需要部署的内容。确认没问题,你就可以将这些变更部署到线上库中。整个部署过程不会导致停机时间,非常方便。

PlanetScale的入门使用是免费的,他们提供了以下免费套餐:

  • 5GB存储空间

  • 每月10亿行读取操作

  • 每月1000万行写入操作

  • 1个生产分支

  • 1个开发分支

  • 社区支持

如果超出了免费套餐的限制,他们会按照以下价格收费:每GB存储空间每月2.5美元,每10亿行读取操作每月1美元,每100万行写入操作每月1.5美元。对于我这样的个人使用者,真的太不错了。

这个平台运行在云上,提供了一个Web管理界面和一个CLI工具。我试了一下他们的Web管理界面,但发现它并不是很好用,无法进行批量的SQL执行。于是我研究了一下CLI工具的使用,并做了一份小记录,现在和大家分享一下。

以下是在 Mac 中使用PlanetScale CLI工具的步骤:

其他系统安装可见:官方文档

1. 安装pscale工具

brew install planetscale/tap/pscale

2. 更新brew和pscale,确保使用的是最新版本

brew update && brew upgrade pscale

3. 进行认证

pscale auth login

这个命令会在浏览器中打开一个页面

image.png

如果你已经登录了PlanetScale账号,它会直接让你确认验证。验证成功后,你就可以开始使用CLI工具了。

如果你走到这步的时候提示:


Error: error decoding error response: invalid character '<' looking for beginning of value

你需要调整一下网络~ 目前是不给大陆用户IP使用的。

4. 连接到相应的数据库分支

pscale connect [数据库名] [分支名] # 例如: pscale connect blog main

连接成功后,你就可以通过本地的3306端口代理访问远程数据库了。


Secure connection to database whrss and branch main is established!.

Local address to connect your application: 127.0.0.1:3306 (press ctrl-c to quit)

image.png

5. 本地连接

点击Get connection strings,你就可以得到连接数据库所需的账号名和密码,然后可以在本地的数据库连接软件中直接连接数据库了。

![](https://raw.githubusercontent.com/whrsss/pic-sync/master/img/202307121100351.png)
  1. 选择适合你的编程语言的连接串,这样你就可以在不同的程序中直接使用了。

    image.png

通过这些简单的步骤,你就可以轻松地使用PlanetScale来管理和部署你的MySQL应用了。快来体验一下吧!

]]>
最近我发现了一个非常有趣的国外MySQL Serverless平台,它叫做PlanetScale。这个平台不仅仅是一个数据库,它能像代码一样轻松地创建开发和测试环境。你可以从主库中拉出一个与之完全相同结构的development或staging数据库,并在这个环境中进行开发和测试。所有的数据都是隔离的,不会相互干扰。 当你完成开发后,你可以创建一个deploy request,PlanetScale会自动比对并生成Schema diff,然后你可以仔细审查需要部署的内容。确认没问题,你就可以将这些变更部署到线上库中。整个部署过程不会导致停机时间,非常方便。
优化你的RSS订阅:一次全面改进的实践 feed-sub 2023-06-26T20:25:21+08:00 2023-06-26T20:25:21+08:00

先前,我的 RSS 订阅功能过于简化,只提供了几个基本字段,而且不展示全文。简介后面,我添加了一个链接指向原文,如下所示:

image.png

过于简陋,无法直接在 RSS 阅读器软件上进行查看,另外,我临时用 Kotlin 编写的这个功能需要每次调用时重新生成,这点就对这个功能的简单和独立性产生了影响,这件事放了太久,最近进行一次全面的改进。

原始的文章是用 Markdown 编写的。为了方便通过手机或电脑上的 RSS 阅读器查看,我需要将文章内容转换为 HTML 格式。在 Go 语言中,有一个成熟的组件:blackfriday。它能把每段文字用 p 标签标记,用 code 和 pre 标签标记代码块,以及用 h1234 标签标记不同层级的标题。


github.com/PuerkitoBio/goquery

github.com/russross/blackfriday/v2

经过一次生成并查看效果后,我发现有些地方需要改进。例如,图片和文字都是左对齐的,看起来并不美观。另外,非代码块的单引号也会被转化为代码块,导致原本可以在一行显示的内容被拆分为三行。于是我添加了一些附加功能:


func Md2Html(markdown []byte) string {



// 1. Convert markdown to HTML

html := blackfriday.Run(markdown)



// 2. Create a new document from the HTML string

doc, err := goquery.NewDocumentFromReader(bytes.NewReader(html))

if err != nil {

log.Fatal(err)

}



// 3. Process all elements to have a max-width of 1300px and text alignment to left

doc.Find("p, h1, h2, h3, h4, h5, h6, ul, ol, li, table, pre").Each(func(i int, s *goquery.Selection) {

s.SetAttr("style", "max-width: 1300px; display: block; margin-left: auto; margin-right: auto; text-align: left;")

})



// 4. Process the images to be centered and have a max size of 500x500

doc.Find("img").Each(func(i int, s *goquery.Selection) {

s.SetAttr("style", "max-width: 500px; max-height: 500px; display: block; margin-left: auto; margin-right: auto;")

})



// 5. Process code blocks to be styled like in markdown, and inline code to be bold

doc.Find("code").Each(func(i int, s *goquery.Selection) {

if goquery.NodeName(s.Parent()) == "pre" {

// this is a code block, keep the markdown style

s.SetAttr("style", "display: block; white-space: pre; border: 1px solid #ccc; padding: 6px 10px; color: #333; background-color: #f9f9f9; border-radius: 3px;")

} else {

// this is inline code, replace it with bold text

s.ReplaceWithHtml("<b>" + s.Text() + "</b>")

}

})



// 6. Get the modified HTML

modifiedHtml, err := doc.Html()

if err != nil {

log.Fatal(err)

}



// Replace self-closing tags

modifiedHtml = strings.Replace(modifiedHtml, "/>", ">", -1)



return modifiedHtml

}

修改之后,我把生成的 HTML 内容放入 RSS 的 XML 中。XML 内容的生成就是简单的字符串拼接,我写了一个 GenerateFeed 方法来完成:


type Article struct {

Title string

Link string

Id string

Published time.Time

Updated time.Time

Content string

Summary string

}



func GenerateFeed(articles []Article) string {

// 对文章按发布日期排序

sort.Slice(articles, func(i, j int) bool {

return articles[i].Published.After(articles[j].Published)

})



// 生成Feed

feed := `<feed xmlns="http://www.w3.org/2005/Atom">

<title>了迹奇有没</title>

<link href="/feed.xml" rel="self"/>

<link href="https://whrss.com/"/>

<updated>` + articles[0].Updated.Format(time.RFC3339Nano) + `</updated>

<id>https://whrss.com/</id>

<author>

<name>whrss</name>

</author>`



for _, article := range articles {

feed += `

<entry>

<title>` + article.Title + `</title>

<link href="` + article.Link + `"/>

<id>` + article.Id + `</id>

<published>` + article.Published.Format(time.RFC3339) + `</published>

<updated>` + article.Updated.Format(time.RFC3339Nano) + `</updated>

<content type="html"><![CDATA[` + article.Content + `]]></content>

<summary type="html">` + article.Summary + `</summary>

</entry>`

}



feed += "\n</feed>"

return feed

}

最后效果确实十分理想,手机上的观看效果不输我的原始站点,很不错~

]]>
先前,我的 RSS 订阅功能过于简化,只提供了几个基本字段,而且不展示全文。我决定对订阅功能做一次全面的改进。
最近在做的事:GitHub Action | GPT Plus | whisper | V2EX | GPT API | PMP done-lately 2023-06-25T15:49:29+08:00 2023-06-25T15:49:29+08:00

运动数据使用GitHub Action自动更新

看着每天的运动数据,满满的,还感觉有点充实:tw-1f606: 积少成多嘛

开通体验了一下GPT Plus

太想体验下GPT4了,之前苦于充值门槛过高,上个月出了app充值后,我使用支付宝礼品卡充值体验了下,感觉还是不错的,但是个人使用确实还是按量更划算一点,但是充值门槛过高:tw-1f632:

买了一支录音笔,全天候录音,使用 whisper 录音转文字

之前全程吃瓜了刘能离婚案,后来受他种草,也买了索尼的tx660,全天候录音,之后再把录音源文件和转的文本保存起来,这样想检索也会很方便的。

终于注册了 V2EX

这个社区知道很久了,一直是游客登录,看到别人发的别人社区啊推荐啊很不错,我说我也得养养号:tw-1f61c:

GPT API续费问题曲线救国

续上面充值问题,最近充值门槛更高了,对网络要求也更高了,还动不动就给封号。那我怎么整,Api确实便宜又方便,而且不用管那么复杂的网络。这不刚注册了V2EX嘛,上去翻翻。

image.png

然后我使用了这个-> aiproxy ,咱这V站注册就开始实际生产力了哈哈哈。

感觉还可以,然后使用自建的客户端输入地址和key就可以用啦。

image.png

报名了PMP的培训班和考试

6月中下旬决定报名8月份的PMP考试,一方面是我现在处于软考之后的一个学习的空窗期,一个原因是因为朋友刘某(一个年轻的项目经理)拉我,最重要的一个原因是,我确实很需要管理协调方面的工作技能,而且我刚好到了PMP的报名门槛:tw-1f604:。

报名费是真滴贵呀~

]]>
运动数据使用GitHub Action自动更新| 开通体验了一下GPT Plus | 买了一支录音笔,全天候录音,使用 whisper 录音转文字 |终于注册了 V2EX |GPT API续费问题曲线救国|报名了PMP的培训班和考试
一个游戏开发者的自我介绍 me 2023-06-08T22:22:30+08:00 2023-06-08T22:22:30+08:00

我写这篇博客的初衷并不是出于单纯的自我表达,而是为了那些可能会对我未来职业发展产生重要影响的人们。或许你是一个正在考虑我的简历的HR,或者是将来可能与我一起共事的团队成员。无论你是谁,我都希望通过这篇博客让你更深入地了解我。同时,写作的过程也让我有机会重新审视和整理我的技能和经历。:tw-1f61d:

我目前就职于一家游戏公司,我们的团队虽然但十分精悍:tw-270a:。我负责的任务非常多,目前核心是一个女性向AVG游戏的后端开发,这款游戏在TapTap上的评价和排名都 ,另外我也参与公共服务和核心业务模块的开发。

除此之外,我还负责管理公司所有的服务器资源,并负责运维工作。这主要得益于我平时对很多技术的尝试和钻研,让我能够顺利应对上云带来的各种挑战。

我在2021年3月加入这家公司,两个月后便开始接手核心项目。主要的技术栈包括Kratos + GoSpring Boot + Kotlin。目前,我们正在将新项目统一使用Go这套技术栈,因为Go不仅满足了我们的需求,还非常易于维护。在数据库方面,我个人对SQL和分布式事务更熟悉,也能够在不同的业务场景中使用不同的数据库和工具。

我加入公司之初,我们的服务和服务器都是单体和独立的,各种技术栈和老项目混乱无章,也恰逢人员交接之际。然而,随着新业务的展开,我们从零开始,大量地构建和改进,同时采用了K8S。如今,无论是服务文档服务框架单元测试,还是CI/CD,我们都已经运行得非常成熟,这也非常适合我们目前和未来一段时间的项目架构。

我始终认为技术人应该持续学习和进步。我觉得我已经做到了我能做到的事情,至于更多的东西,我知道我现在的知识储备可能有限,但我仍希望能更进一步。年轻的我没有太多负担,希望能够抓住机会,走得更远。这就是我正在寻找新机会的原因。我期待挑战,也期待更有趣的工作。我相信,无论结果如何,这个过程都会带给我宝贵的经验和成长。

]]>
职业道路上的新旅程:一个游戏开发者的自我介绍
使用GitHub Action 同步运动数据并生成热力图 github-poster 2023-06-04T21:14:48+08:00 2023-06-04T21:14:48+08:00

如果你想把你的运动数据和热力图同步到 GitHub,那么你来对地方了。在这篇文章中,我将详细解释如何使用 GitHub Actions 和 Python 自动同步和更新你的数据。

这个项目是在伊洪@yihong0618的GitHubPoster的基础上进行的所以在此表示感谢。他的另一个项目是IBeat (之前我对GitHub Action做了一次尝试),从这里我开始接触了快捷指令的触发。

最终效果如下:

我的运动数据

一. 开始前的准备

首先,你需要 fork @yihong0618的项目,并将其克隆到本地: https://github.com/whrsss/GitHubPoster

然后,安装项目的依赖项:


pip3 install -r requirements.txt

二. 全量生成历史数据

研究了一下,步骤是先全量生成历史数据(用全量模式backfill),再进行追加(incremental)。

下面是如何导出每日运动量(卡路里)出来的例子。首先,将apple 运动中的数据导出,解压在IN_FOLDER文件夹中,然后在项目根目录运行:


python3 -m github_poster apple_health --apple_health_mode backfill --year 2020-2023 --apple_health_record_type move --me "your name"

这样在OUT_FOLDER目录下就会生成一份全量的数据,IN_FOLDER下也会生成对应的全量json文件,之后的增量,会加入到IN_FOLDER的json文件中。

逻辑微调

由于我的目标是统计每日消耗的卡路里,在apple health导出的数据中,分成了两块儿, 分别是

type=“HKQuantityTypeIdentifierBasalEnergyBurned”和 type=“HKQuantityTypeIdentifierActiveEnergyBurned” ,每日基础消耗数据和每日运动活动数据。

(ps. Apple 运动记录中不同的运动类型在这里,想要自定义其他可以自取)

而在GitHubPoster中支持的3种类型中,move、exercise、stand,对应的apple health数据都是单种类型,所以,需要将type修改为数组类型才能实现。

在 github_poster/loader/apple_health_loader.py 中修改其中的 RecordMetadata 的支持和使用。

首先需要将type字段修改为一个列表或者集合。在定义RecordMetadata时,将type字段类型改为ListSet


from typing import List



RecordMetadata = namedtuple("RecordMetadata", ["types", "unit", "track_color", "func"])

然后在定义HEALTH_RECORD_TYPES时,你就可以为每个键提供多个类型了:


HEALTH_RECORD_TYPES = {

    "stand": RecordMetadata(

        ["HKCategoryTypeIdentifierAppleStandHour", "AnotherType", ...],

        "hours",

        "#62F90B",

        lambda x: 1 if "HKCategoryValueAppleStandHourStood" else 0,

    ),

    ...

}

接着,你需要在AppleHealthLoader类的backfill方法中,修改判断记录类型的逻辑。将原来的等于比较改为检查类型是否在types列表中:


def backfill(self):

    from_export = defaultdict(int)



    in_target_section = False

    for _, elem in ET.iterparse(self.apple_health_export_file, events=["end"]):

        if elem.tag != "Record":

            continue



        if elem.attrib["type"] in self.record_metadata.types:

            ...

这样,每个RecordMetadata就可以包含多个type了,可以在HEALTH_RECORD_TYPES中为每个键定义任意数量的类型。

再次执行,即可生成想要的数据。

到这里,如果你不需要每日更新,只发个朋友圈啥的,就够了。


三. 设置 GitHub Actions 进行数据的增量更新

要实现数据的每日更新,我们需要利用快捷指令和 GitHub Actions。具体的步骤如下:

生成一个AccessToken

我们首先需要生成一个AccessToken,用来作为快捷指令post请求的凭证: image.png

配置GitHub仓库的权限

接下来,我们需要打开GitHub action 的仓库读写权限,用来作为图片生成完成以及数据增量的仓库保存: image.png

编写 GitHub Actions workflow 编写 GitHub Actions 工作流程

我们在.github/workflow目录下创建一个yml后缀的文件,比如叫sync_exercise.yml,下面是我的文件的内容:


name: Run Poster Generate

on:

  workflow_dispatch:

    inputs:

      time:

        description: 'time list'

        required: false

      value:

        description: 'value list'

        required: false

  pull_request:

  

env:

  TYPE: "apple_health" 

  ME: whrsss

  # change env here

  GITHUB_NAME: whrsss

  GITHUB_EMAIL: xxx@qq.com

jobs:

  sync:

    name: Sync

    runs-on: ubuntu-latest

    steps:

      - name: Checkout

        uses: actions/checkout@v3

      - name: Set up Python

        uses: actions/setup-python@v4

        with:

          python-version: 3.8



      - name: Install dependencies

        run: |

          python -m pip install --upgrade pip

          pip install -r requirements.txt

        if: steps.pip-cache.outputs.cache-hit != 'true'



      - name: Run sync apple_health script

        if: contains(env.TYPE, 'apple_health')

        run: |

          python3 -m github_poster apple_health --apple_health_mode incremental --apple_health_date ${{ github.event.inputs.time }} --apple_health_value ${{ github.event.inputs.value }} --year 2020-2023 --apple_health_record_type move --me "whrsss"



      - name: Push new poster

        run: |

          git config --local user.email "${{ env.GITHUB_EMAIL }}"

          git config --local user.name "${{ env.GITHUB_NAME }}"

          git add .

          git commit -m 'update new poster' || echo "nothing to commit"

          git push || echo "nothing to push"



如果想要了解github action语法的同学可以google一下

上面的yaml的步骤分别是:

  1. 定义了一个名为 “Run Poster Generate” 的 Github Actions

  2. 指定了两种触发方式,一种是手动触发的 workflow_dispatch,另一种是当有新的 pull_request 时触发

  3. 指定了一些环境变量,包括 poster 生成所需的参数

  4. 定义了一个名为 “sync” 的 job,指定在 ubuntu-latest 操作系统上运行

  5. 包括了三个步骤:

    • checkout:将代码仓库从 Github 上下载到本地

    • set up python:安装 python 3.8 版本

    • install dependencies:安装项目所需的所有依赖包

  6. 如果缓存中没有依赖,才会继续执行后续步骤

  7. 如果环境变量中的 TYPE 是 apple_health,则执行同步 Apple Health 数据的脚本

  8. 完成同步后,将生成的新 poster 推送到 Github 仓库中去。

获取workflow id 获取workflow id

我们可以通过以下命令获取workflow id:


curl https://api.github.com/repos/{用户名}/{仓库名}/actions/workflows -H "Authorization: token d8xxxxxxxxxx" # change to your config

上面的token后的token就是第一步获取的token。

image.png

创建快捷指令

你可以下载大佬已经创建好的快捷指令,然后修改以下部分即可:

设置自动化

最后,我们需要设置快捷指令的自动触发条件。我选择的是每天结束时进行同步: 54781685519843_.pic.jpg

结束语

希望这篇文章对你有所帮助。如果你试过这个项目,我很乐意听到你的反馈和经验。如果你有任何问题或建议,也欢迎在下面的评论中提出。

]]>
如果你想把你的运动数据和热力图同步到 GitHub,那么你来对地方了。在这篇文章中,我将详细解释如何使用 GitHub Actions 和 Python 自动同步和更新你的数据。 这个项目是在伊洪@yihong0618的GitHubPoster的基础上进行的所以在此表示感谢。他的另一个项目是IBeat (之前我对GitHub Action做了一次尝试),从这里我开始接触了快捷指令的触发。
【Gorm】Save方法更新踩坑记录 gorm-save-issue 2023-06-03T16:53:55+08:00 2023-06-03T16:53:55+08:00

在我最近使用Gorm进行字段更新的过程中,我遇到了一个问题。当我尝试更新status字段时,即使该字段的值没有发生变化,Gorm还是提示我“Duplicate entry ‘xxxx’ for key ‘PRIMARY’”。

首先,让我们看看Gorm的官方文档对Save方法的描述:

Save方法会保存所有的字段,即使字段是零值。


db.First(&user)  



user.Name = "jinzhu 2"  

user.Age = 100  

db.Save(&user)  

// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;  



```  



`Save`方法是一个复合函数。如果保存的数据不包含主键,它将执行`Create`。反之,如果保存的数据包含主键,它将执行`Update`(带有所有字段)。



```go

db.Save(&User{Name: "jinzhu", Age: 100})  

// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")  



db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})  

// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1  



根据这个描述,我预期的行为应该是更新操作,因为我提供了ID字段。然而,实际发生的却是插入操作。这让我感到困惑。

为了理解这个问题,我深入阅读了Gorm的源码:


// Save updates value in database. If value doesn't contain a matching primary key, value is inserted.func (db *DB) Save(value interface{}) (tx *DB) {  

tx = db.getInstance()  

tx.Statement.Dest = value  

  

reflectValue := reflect.Indirect(reflect.ValueOf(value))  

for reflectValue.Kind() == reflect.Ptr || reflectValue.Kind() == reflect.Interface {  

reflectValue = reflect.Indirect(reflectValue)  

}  

  

switch reflectValue.Kind() {  

case reflect.Slice, reflect.Array:  

if _, ok := tx.Statement.Clauses["ON CONFLICT"]; !ok {  

tx = tx.Clauses(clause.OnConflict{UpdateAll: true})  

}  

tx = tx.callbacks.Create().Execute(tx.Set("gorm:update_track_time", true))  

case reflect.Struct:  

if err := tx.Statement.Parse(value); err == nil && tx.Statement.Schema != nil {  

for _, pf := range tx.Statement.Schema.PrimaryFields {  

if _, isZero := pf.ValueOf(tx.Statement.Context, reflectValue); isZero {  

return tx.callbacks.Create().Execute(tx)  

}  

}  

}  

  

fallthrough  

default:  

selectedUpdate := len(tx.Statement.Selects) != 0  

// when updating, use all fields including those zero-value fields  

if !selectedUpdate {  

tx.Statement.Selects = append(tx.Statement.Selects, "*")  

}  

  

updateTx := tx.callbacks.Update().Execute(tx.Session(&Session{Initialized: true}))  

  

if updateTx.Error == nil && updateTx.RowsAffected == 0 && !updateTx.DryRun && !selectedUpdate {  

return tx.Create(value)  

}  

  

return updateTx  

}  

  

return  

}

源码的主要逻辑如下:

  1. 获取数据库实例并准备执行SQL语句。value是要操作的数据。

  2. 利用反射机制确定value的类型。

  3. 如果value是Slice或Array,并且没有定义冲突解决策略(”ON CONFLICT”),那么设置更新所有冲突字段的冲突解决策略,并执行插入操作。

  4. 如果value是一个Struct,那么会尝试解析这个结构体,然后遍历它的主键字段。如果主键字段是零值,则执行插入操作。

  5. 对于除Slice、Array、Struct以外的类型,将尝试执行更新操作。如果在更新操作后,没有任何行受到影响,并且没有选择特定的字段进行更新,则执行插入操作。

从这个函数我们可以看出,当传入的value对应的数据库记录不存在时(根据主键判断),Gorm会尝试创建一个新的记录。如果更新操作不影响任何行,Gorm同样会尝试创建一个新的记录。

这个行为与我们通常理解的“upsert”(update + insert)逻辑略有不同。在这种情况下,即使更新的数据与数据库中的数据完全相同,Gorm还是会尝试进行插入操作。这就是为什么我会看到Duplicate entry 'xxxx' for key 'PRIMARY'的错误,因为这就是主键冲突的错误提示。

对于Gorm的这种行为我感到困惑,同时我也对官方文档的描述感到失望,因为它并没有提供这部分的信息。

如何解决这个问题呢?

我们可以自己实现一个Save方法,利用GORM的Create方法和冲突解决策略:


// Update all columns to new value on conflict except primary keys and those columns having default values from sql func  

db.Clauses(clause.OnConflict{  

  UpdateAll: true,  

}).Create(&users)  

// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;  

// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age), ...; MySQL



在Gorm的Create方法的文档中,我们可以看到这种用法。如果提供了ID,它会更新其他所有的字段。如果没有提供ID,它会插入新的记录。

]]>
在我最近使用Gorm进行字段更新的过程中,我遇到了一个问题。当我尝试更新status字段时,即使该字段的值没有发生变化,Gorm还是提示我“Duplicate entry 'xxxx' for key 'PRIMARY'”。
一次线上异常的追踪与处理 exception-tracking 2023-06-02T19:24:29+08:00 2023-06-02T19:24:29+08:00

一次线上异常的追踪与处理

5月31日晚,我们接到游戏玩家反馈,经常出现请求超时的提示。在我亲自登录游戏验证后,也出现了相同的错误,但游戏仍然可以正常运行,数据也没有任何问题。

经过客户端的错误检查,我们发现请求出现了408 Request Timeout的错误。该响应状态码意味着服务器打算关闭没有在使用的连接,即使客户端没有发送任何请求,一些服务器仍会在空闲连接上发送此信息。服务器决定关闭连接,而不是继续等待。

1. 日志检查

接下来,我查看了服务器的日志,发现后台的两个服务的日志都在正常运行,没有异常提示。当我进行pod查看时,发现有两个pod显示容器没有日志,这两个pod已经挂掉。

为什么这两个pod会宕机呢?我开始回溯近1小时的日志,发现在晚上10点左右,出现了JDBC连接异常。


### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 31363ms.

通过Google查询,我了解到这种错误是由于Spring Boot的默认连接池HikariPool在连接排队阻塞,无法获取连接,最后导致超时。在数据库错误之后的一段时间内,出现了Java内存异常。


{"@timestamp":"2023-05-31 22:18:24.382","level":"ERROR","source":{"className":"org.apache.juli.logging.DirectJDKLog","methodName":"log","line":175},"message":"Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space] with root cause","error.type":"java.lang.OutOfMemoryError","error.message":"Java heap space","error.stack_trace":"java.lang.OutOfMemoryError: Java heap space\n"}

由于我们没有设置连接池上限(默认最大为10),当获取连接阻塞后,请求排队,最终导致内存溢出。最后,由于内存溢出,pod触发java.io.IOException: Broken pipe错误,即管道断开,服务宕机。


{"@timestamp":"2023-05-31 22:18:24.393","level":"WARN","source":{"className":"org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver","methodName":"logException","line":199},"message":"Resolved [org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space]"}

2. 增加连接池大小

Hikari是Spring Boot自带的连接池,默认最大只有10个。因此,我的第一步解决方案是增加这个服务的连接池大小。在服务的yaml数据库连接配置中增加了一些参数。 


  datasource:

    url: 'jdbc:mysql://rm-2xxxxxx'

    username: 'xx'

    password: 'xxx'

    # 下面这些????

    type: com.zaxxer.hikari.HikariDataSource

    driver-class-name: com.mysql.cj.jdbc.Driver

    hikari:

      #连接池名

      pool-name: DateHikariCP

      #最小空闲连接数

      minimum-idle: 10

      # 空闲连接存活最大时间,默认600000(10分钟)

      idle-timeout: 180000

      # 连接池最大连接数,默认是10

      maximum-pool-size: 100

      # 此属性控制从池返回的连接的默认自动提交行为,默认值:true

      auto-commit: true

      # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟

      max-lifetime: 1800000

      # 数据库连接超时时间,默认30秒,即30000

      connection-timeout: 30000

      connection-test-query: SELECT 1



3. 性能优化

尽管从表面上看,问题是由于连接池数量太少,导致连接请求阻塞。但深层的原因是服务对数据库的请求处理过慢,最后导致阻塞。如果请求数量继续增加,即使扩大了连接池,同样会阻塞连接。这就像滴滴打车,碰到下雨天儿,队一旦开始排,后面就不知道要排多久了。

这个数据库存储了大量的数据,其中聊天记录的存储主要占用了性能。我们在处理聊天记录时做了分表处理。但由于数据量过大,单表依然有近两千万的数据。这张大表有一个联合索引,索引数据量较大。每次更新都需要维护索引空间,每次单个玩家数据量到达限值,就会进行局部清理。

这里的数据插入动作可能消耗时间较长。由于对消息的可靠性要求不高,我们可以使用异步进行,这样在等待插入的过程中可以省去大量的请求连接占用资源。

我们优化了消息保存数量。以前,每个玩家保存900条消息,但一般只查询最近的300条。现在,每500条进行一次清理,清理至300条,以节省空间。

即使数据量节省了很多,但由于业务价值相对成本比例因素,与业务部门进行沟通,将业务的容忍度定为定期3个月。

4. 空间优化

查询表空间占用:


-- 查询库中每个表的空间占用,分项列出 

select   table_schema as '数据库',   table_name as '表名',   table_rows as '记录数',   truncate(data_length/1024/1024, 2) as '数据容量(MB)',   truncate(index_length/1024/1024, 2) as '索引容量(MB)'   

from information_schema.tables   

where table_schema='表名'   

order by data_length desc, index_length desc;

对经常有删除操作的数据表进行碎片清理:


alter table 表名 engine=innodb;

经过清理,可以看到表空间占用缩小了40%左右。加上之前的业务修改,数据量又有了明显的缩减,使得数据库到了MySQL的舒适区,单表在500万左右。

5. 相关经验

以前我们有一个Go服务有非常大的IO,偶尔会出现崩溃,日志也是提示:“write tcp IP: xxx-> IP:xxx write: broken pipe”。开始以为是服务器在上传到OSS的过程中出现的连接异常,后来和阿里确认了并非OSS的断开错误。经过多次排查,最后发现在上传文件前,对内容进行了json序列化,这个过程非常费性能。当请求过多时,就发生了阻塞,阻塞过多,内存占用过大,溢出,服务就会拒绝服务。此时,连接的管道就会强行断开。

在很多业务场景中,都会出现这种情况:当计算资源不足时,请求就会阻塞堆积,最后最先崩溃的总是内存。

]]>
5月31日晚,我们接到游戏玩家反馈,经常出现请求超时的提示。在我亲自登录游戏验证后,也出现了相同的错误,但游戏仍然可以正常运行,数据也没有任何问题。 经过客户端的错误检查,我们发现请求出现了`408 Request Timeout`的错误。该响应状态码意味着服务器打算关闭没有在使用的连接,即使客户端没有发送任何请求,一些服务器仍会在空闲连接上发送此信息。服务器决定关闭连接,而不是继续等待。
Go 单元测试高效实践 go-test 2023-04-21T18:27:10+08:00 2023-04-21T18:27:10+08:00

敏捷开发中有一个广为人知的开发方法就是 XP(极限编程),XP 提倡测试先行,为了将以后出现 bug 的几率降到最低,这一点与近些年流行的 TDD(测试驱动开发)有异曲同工之处。

在最开始做编程时,我总是忽略单元测试在代码中的作用,觉得编写单元测试的功夫都赶上甚至超越业务程序了。到后来,业务量越来越复杂,慢慢地,浮现一个问题,就是系统对于测试人员是一个黑盒,简单的测试无法保证系统所设计的东西都可以测试到⬇️

举两个最简单的例子:

系统设计的数据打点,是无法从功能业务上测试出来的,而对于测试人员,可能由于版本差异,用例未覆盖。

如果一个表中有两个字段,新用户过来更新一个字段之后,测另一个字段的功能时就不再以一个新用户的身份操作了。

在这样的情况下,如果开发人员没有对系统做完全的检查,就很可能出现问题。

就以上情况看,需要从开发人员的维度,对功能做一个“预期”测试,一个功能走过,应该输入什么,输出什么,哪些数据变动了,变动是否符合预期等等。

最近,公司业务基本都转入了 Go 做开发,在 Go 的整个业务处理上也日渐完善,而 Go 的单元测试用起来也十分顺手,所以做个小的总结。

一. Mock DB

在单元测试中,很重要的一项就是数据库的 Mock,数据库要在每次单元测试时作为一个干净的初始状态,并且每次运行速度不能太慢。

1. Mysql 的 Mock

这里使用到的是 github.com/dolthub/go-mysql-server 借鉴了这位大哥的方法 如何针对 MySQL 进行 Fake 测试

  • ###### DB 的初始化

在 db 目录下


type Config struct {

   DSN             string // write data source name.

   MaxOpenConn     int    // open pool

   MaxIdleConn     int    // idle pool

   ConnMaxLifeTime int

}



var DB *gorm.DB



// InitDbConfig 初始化Db

func InitDbConfig(c *conf.Data) {

   log.Info("Initializing Mysql")

   var err error

   dsn := c.Database.Dsn

   maxIdleConns := c.Database.MaxIdleConn

   maxOpenConns := c.Database.MaxOpenConn

   connMaxLifetime := c.Database.ConnMaxLifeTime

   if DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{

      QueryFields: true,

      NamingStrategy: schema.NamingStrategy{

         //TablePrefix:   "",   // 表名前缀

         SingularTable: true, // 使用单数表名

      },

   }); err != nil {

      panic(fmt.Errorf("初始化数据库失败: %s \n", err))

   }

   sqlDB, err := DB.DB()

   if sqlDB != nil {

      sqlDB.SetMaxIdleConns(int(maxIdleConns))                               // 空闲连接数

      sqlDB.SetMaxOpenConns(int(maxOpenConns))                               // 最大连接数

      sqlDB.SetConnMaxLifetime(time.Second * time.Duration(connMaxLifetime)) // 单位:秒

   }

   log.Info("Mysql: initialization completed")

}

  • ###### fake-mysql 的初始化和注入

在 fake_mysql 目录下


var (

   dbName    = "mydb"

   tableName = "mytable"

   address   = "localhost"

   port      = 3380

)



func InitFakeDb() {

   go func() {

      Start()

   }()

   db.InitDbConfig(&conf.Data{

      Database: &conf.Data_Database{

         Dsn:             "no_user:@tcp(localhost:3380)/mydb?timeout=2s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4",

         ShowLog:         true,

         MaxIdleConn:     10,

         MaxOpenConn:     60,

         ConnMaxLifeTime: 4000,

      },

   })

   migrateTable()

}



func Start() {

   engine := sqle.NewDefault(

      memory.NewMemoryDBProvider(

         createTestDatabase(),

         information_schema.NewInformationSchemaDatabase(),

      ))



   config := server.Config{

      Protocol: "tcp",

      Address:  fmt.Sprintf("%s:%d", address, port),

   }



   s, err := server.NewDefaultServer(config, engine)

   if err != nil {

      panic(err)

   }



   if err = s.Start(); err != nil {

      panic(err)

   }



}



func createTestDatabase() *memory.Database {

   db := memory.NewDatabase(dbName)

   db.EnablePrimaryKeyIndexes()

   return db

}



func migrateTable() {

// 生成一个user表到fake mysql中

   err := db.DB.AutoMigrate(&model.User{})

   if err != nil {

      panic(err)

   }

}

在单元测试开始,调用 InitFakeDb() 即可


func setup() {

   fake_mysql.InitFakeDb()

}


2. Redis 的 Mock

这里用到的是 miniredis , 与之配套的Redis Client 是 go-redis/redis/v8 ,在这里调用 InitTestRedis() 注入即可


// RedisClient redis 客户端  

var RedisClient *redis.Client  

  

// ErrRedisNotFound not exist in redisconst ErrRedisNotFound = redis.Nil  

  

// Config redis config

type Config struct {  

   Addr         string  

   Password     string  

   DB           int  

   MinIdleConn  int  

   DialTimeout  time.Duration  

   ReadTimeout  time.Duration  

   WriteTimeout time.Duration  

   PoolSize     int  

   PoolTimeout  time.Duration  

   // tracing switch  

   EnableTrace bool  

}  

  

// Init 实例化一个redis client  

func Init(c *conf.Data) *redis.Client {  

   RedisClient = redis.NewClient(&redis.Options{  

      Addr:         c.Redis.Addr,  

      Password:     c.Redis.Password,  

      DB:           int(c.Redis.DB),  

      MinIdleConns: int(c.Redis.MinIdleConn),  

      DialTimeout:  c.Redis.DialTimeout.AsDuration(),  

      ReadTimeout:  c.Redis.ReadTimeout.AsDuration(),  

      WriteTimeout: c.Redis.WriteTimeout.AsDuration(),  

      PoolSize:     int(c.Redis.PoolSize),  

      PoolTimeout:  c.Redis.PoolTimeout.AsDuration(),  

   })  

  

   _, err := RedisClient.Ping(context.Background()).Result()  

   if err != nil {  

      panic(err)  

   }  

  

   // hook tracing (using open telemetry)  

   if c.Redis.IsTrace {  

      RedisClient.AddHook(redisotel.NewTracingHook())  

   }  

  

   return RedisClient  

}  

  

// InitTestRedis 实例化一个可以用于单元测试的redis  

func InitTestRedis() {  

   mr, err := miniredis.Run()  

   if err != nil {  

      panic(err)  

   }  

   // 打开下面命令可以测试链接关闭的情况  

   // defer mr.Close()  

  

   RedisClient = redis.NewClient(&redis.Options{  

      Addr: mr.Addr(),  

   })  

   fmt.Println("mini redis addr:", mr.Addr())  

}

二. 单元测试

经过对比,我选择了 goconvey 这个单元测试框架

它比原生的go testing 好用很多。goconvey还提供了很多好用的功能:

  • 多层级嵌套单测

  • 丰富的断言

  • 清晰的单测结果

  • 支持原生go test

使用


go get github.com/smartystreets/goconvey


func TestLoverUsecase_DailyVisit(t *testing.T) {  

   Convey("Test TestLoverUsecase_DailyVisit", t, func() {  

      // clean  

      uc := NewLoverUsecase(log.DefaultLogger, &UsecaseManager{})  

  

      Convey("ok", func() {  

         // execute  

         res1, err1 := uc.DailyVisit("user1", 3)  

         So(err1, ShouldBeNil)  

         So(res1, ShouldNotBeNil)  

         // 第 n (>=2)次拜访,不应该有奖励,也不应该报错  

         res2, err2 := uc.DailyVisit("user1", 3)  

         So(err2, ShouldBeNil)  

         So(res2, ShouldBeNil)  

      })  

   })  

}

可以看到,函数签名和 go 原生的 test 是一致的

测试中嵌套了两层 Convey,外层new了内层Convey所需的参数

内层调用了函数,对返回值进行了断言

这里的断言也可以像这样对返回值进行比较 So(x, ShouldEqual, 2)

或者判断长度等等 So(len(resMap),ShouldEqual, 2)

Convey的嵌套也可以灵活多层,可以像一棵多叉树一样扩展,足够满足业务模拟。


三. TestMain

为所有的 case 加上一个 TestMain 作为统一入口


import (  

"os"  

"testing"  

  

. "github.com/smartystreets/goconvey/convey"  

)  

  

func TestMain(m *testing.M) {  

   setup()  

   code := m.Run()  

   teardown()  

   os.Exit(code)

}

// 初始化fake db

func setup() {  

   fake_mysql.InitFakeDb()  

   redis.InitTestRedis()

}

]]>
敏捷开发中有一个广为人知的开发方法就是 XP(极限编程),XP 提倡测试先行,为了将以后出现 bug 的几率降到最低,这一点与近些年流行的 TDD(测试驱动开发)有异曲同工之处。 在最开始做编程时,我总是忽略单元测试在代码中的作用,觉得编写单元测试的功夫都赶上甚至超越业务程序了。到后来,业务量越来越复杂,慢慢地,浮现一个问题,就是系统对于测试人员是一个黑盒,简单的测试无法保证系统所设计的东西都可以测试到。
一个有趣且实用的开源项目,将网页打包成应用 pake 2023-04-04T16:19:46+08:00 2023-04-04T16:19:46+08:00

最近在GitHub上面发现了一个很有趣的开源应用,可以将网页打包成应用:

这里是它的使用文档

底层使用了rust进行开发,支持 Mac、Windows 和 Linux。使用命令将网站直接打包成应用。

这里我以最近部署的一个私人 ChatGPT Next为例, 我的地址为 https://gpt.whrss.com

需要本地先有一个node环境,能使用npm命令。

然后npm全局安装这个工具


npm install -g pake-cli

安装完之后,就可以使用


pake url [OPTIONS]...

来生成桌面应用了,参数有很多,我先用了这些:

这是我的命令:


pake https://gpt.whrss.com --name GPT  --icon /Users/wu/Downloads/gpt.icns --height 700 --width 900

这里我指定了网站的url : https://gpt.whrss.com

指定了生成的应用包的名字:GPT

指定了应用的 icon

指定了应用打开初始化的高度和宽度

mac icon生成和获取的地方 : https://macosicons.com/#/

然后我进行了运行,因为我的icon指定的目录是Downloads下面,所以它直接生成在了下面,默认是用户目录。

image.png

如上图,生成的包体非常的小和轻量,只有不到3M。

= 更新 =


# 查看npm安装的插件的版本

npm -g list

/usr/local/lib

├── xxx

├── npm@9.3.1

├── pake-cli@2.0.0-alpha7

├── xxxxxxx

└── xxxxxxx



# 更新指定插件

npm -g update pake-cli 

# 再次查看 

npm -g list


关于这个软件的应用,我想到的最多就是那些常用的,但没有pc客户端的应用,比如 Twitter、WeRead、Spotify 等等,我也是接触这个项目后第一时间就使用上了,体验很不错,比之前方便了不少。

image.png

]]>
最近在GitHub上面发现了一个[很有趣的开源应用](https://github.com/tw93/Pake),可以将网页打包成应用 底层使用了rust进行开发,支持 Mac、Windows 和 Linux。使用命令将网站直接打包成PC应用。
GitHub Action 自动化部署简单尝试 github-action 2023-03-31T18:38:20+08:00 2023-03-31T18:38:20+08:00

之前看到很多大佬的 Blog 是部署在 Github 上面的,但因为自己目前的博客是带后端的,所以就没有考虑。很久之前看到 @yihong 的心跳和跑步,感觉挺不错的,但因为自己没有跑步的习惯,就感觉不是很感冒 ???? 直到最近在听零机一动的时候,又听到了 yihong 的跑步, 我突然想到,我应该也可以把我的游泳 骑车 有氧也像 @yihong 的跑步数据一样上传过来。那么第一件事,我需要了解一下 GitHub Action 的机制,小小地尝试一下。


首先,先看下 github action 的基本功能,我是通过阮一峰老师的文章了解的。

GitHub Actions 是一种自动化工具,2019 年开始测试,同年 11 月上线。可以在 GitHub 上构建、测试和部署软件项目。它允许 Github 用户通过预定义或自定义的操作序列来自动化整个软件开发生命周期中的流程(过程很像 Dockerfile 或 Jenkinsfile 的构建),并且完全与 Github 集成。

使用 GitHub Actions,你可以创建一个由事件触发或定期运行的工作流(workflow),其中包含一个或多个步骤(steps)。每个步骤都是一个特定任务的命令,例如编译代码、运行测试、打包发布版本等。你可以选择将这些步骤放在同一个工作流内,也可以将它们拆分为不同的工作流文件。

此外,Github Action 还提供了各种操作 (actions),可以让您轻松地执行常见的任务,如 Shell 脚本、推送 Docker 映像、调用 API 等等。 也有一些和捷径市场一样的分享社区,把自己编写的 action yml 分享给他人使用。

GitHub Actions 使得整个开发过程变得更加高效方便,能够提升团队的交付速度。同时,近年来越来越多的开源社区也开始尝试在其上面构建 CI/CD 流水线。

这里可能没有接触过的朋友可能会想,我是否可以在上面部署应用呢?

这里的 Action 的作用类似于 Jenkins,可以帮你打包编译,如果你是一个静态项目,那么作为静态仓库的 Github 仓库是可以直接部署的; 但是如果是动态的后端服务,需要 Cpu 的,就无法进行了。


第一步,创建 GitHub Token

头像 -> settings -> 左侧 Developer settings -> 左侧 Personal access tokens -> Tokens(classic)

-> Generate new token (classic)

赋予 workflow 的权限

第二步: 在目标仓库中创建  .github/workflows  目录

目录中创建一个 yml 文件 workflow-name.yml

按照 GitHub 文档进行 yml 编写,我这里先了解机制,所以直接 fork @yihong 的项目,

第三步 Action 设置

在项目的 settings 中找到 actions 下的 general,向下划动

image.png

image.png

找到工作流的读写权限,进行勾选。这样我们在触发 action 后,项目的 push 就有权限进行了。

第四步,触发这个 Action

在手机上部署这个捷径

Token 填写第一步获取到的Token

GitHub_Name 填写Github用户名

Action_ID 填写yml名字(workflow-name.yml就全部填上去)

修改仓库名字和分支(图中的whrsss是我的仓库名字,master是我的分支名)

image.png

修改完成后,点击快捷指令的运行。然后打开Github 仓库,点开 Action 栏,项目正在构建。点开可以查看构建日志。

image.png

]]>
之前看到很多大佬的 Blog 是部署在 Github 上面的,但因为自己目前的博客是带后端的,所以就没有考虑。很久之前看到 @yihong 的心跳和跑步,感觉挺不错的,但因为自己没有跑步的习惯,就感觉不是很感冒 ???? 直到最近在听零机一动的时候,又听到了 yihong 的跑步, 我突然想到,我应该也可以把我的游泳 骑车 有氧也像 @yihong 的跑步数据一样上传过来。那么第一件事,我需要了解一下 GitHub Action 的机制,小小地尝试一下。
从零开始搭建家庭软路由系统(安装OpenWrt,并做为旁路由接入家庭网络) soft-routes 2023-03-21T14:09:57+08:00 2023-03-21T14:09:57+08:00

之前刷推看到了不少人发软路由,最近又看到a姐发了一句:全推入手软路由了

开始我还觉得软路由对我的作用应该不大吧,随着从众心理的影响,我觉得我应该试试。

刚好我还有从笔记本上拆下的内存和固态,岂不是严丝合缝?

然后我就下单了一个50091679362219_.pic.jpg

外观差不多是这个样子:

50101679362338_.pic.jpg

50111679362342_.pic.jpg

内部结构大概是这个样子:

50121679362346_.pic.jpg

拥有5个网口、两个 USB 2.0 和两个 USB 3.0 口、HDMI、DP、存储卡位以及两个笔记本内存条槽位、一根 m.2 插槽和一个 SATA 接口,可以说五脏俱全。

我买的是只带电源的,内存条和磁盘需要自己加。

一、第一步 组装机器

  • 打开机器背部的固定螺丝以及后盖,将内存条插入相应槽位

  • 接入 SATA 固态硬盘或 M.2 硬盘,不要装上后盖,以防有问题还需要再拆开。

  • 连接电源,此时路由器将自动开机

  • 开机后如果听到一声响,表示内存识别功能正常。如果听到三声响连续作响就表示出现了问题。我使用的两个内存条,分别在两个槽位上尝试,只有一根(金士顿的骇客神条2400)能在其中一个槽位上被识别。如果两个槽位都无法识别,就只能买同店的内存条了。

50131679363062_.pic.jpg

  • 接下来就是准备安装系统。

二、 制作启动盘

我选择的系统是 OpenWRT/LEDE 在国内的家庭软路由中有着非常高的占有率,拥有海量的软件,和非常强大的生态。同时,OpenWRT 的教程也很丰富详实。

这里我使用的是 KoolShare 固件,内置了非常强大的插件市场。

1. 下载efi

image.png

image.png

找到最新版本进行下载。

image.png

2. 写盘

下载完成后,使用balenaetcher写盘工具进行写盘,

  • 如果没有安装balenaEtcher,首先需要下载并安装。

  • 插入你的U盘,并打开balenaEtcher。

  • 点击“选择镜像”按钮,选择刚才下载的.gz文件。

  • 点击“选择驱动器”按钮,选择你插入的U盘。

  • 点击“写入”按钮,balenaEtcher将开始写入镜像文件到你的U盘。

  • 等待写入过程完成,并在完成后安全地弹出。

三、 安装和设置

1. 安装
  • 将写好的盘插入软路由,接一个显示器和一个键盘给软路由,接上电源,开机。按f11(不同机器不同快捷键)进入快速启动。一切自动执行到终端提示完成,回车,提示出OpenWrt图标。

  • 输入quickstart,1 设置Lan口IP,2 安装OpenWrt,3 重置

  • 选择2安装操作系统。这时候会提示安装位置,如果识别磁盘没有问题,就会让你选择你内部的固态或者sata硬盘。如果提示没有找到硬盘,就是硬盘有问题或者接口接错了(我在使用sata协议的m.2接口固态硬盘时,提示了这个问题,固态硬盘需要选择nvme接口协议的)

  • 几秒后会提示你拔掉U盘,这时候就会自动重启进入系统。

  • 同样的 输入quickstart 选择1 设置lan口IP。可以设置为 192.168.2.6, 子网掩码设置为255.255.255.0(等下会说明用途)

  • 接一根网线从软路由 lan 口到 PC 上,pc打开浏览器,访问 192.168.2.6 ,这时候会进入软路由后台,用户名root 默认密码 password

到这里,安装基本完成,openWrt可以进入后台访问。

2. 设置旁路由

旁路由就是将软路由接入在普通硬路由下,做数据处理。

image.png

  • 在硬路由器控制台上禁用 DHCP 功能,然后记录下其 LAN 口 IP 地址(例如,我的是 192.168.2.5)

  • 在软路由面板的“网络”下,选择“接口”并设置 LAN 口,将 LAN 口 IP 设置为硬路由器 IP 同一网段(也就是192.168.2.6),将网关地址和 DNS 解析地址都设置为硬路由器 LAN 口地址 192.168.2.5 点击保存并应用。

拔下PC网线,将软路由Lan口与硬路由Lan口相连。

电脑连接原先的硬路由wifi网络, 浏览器输入192.168.2.6,正确访问到软路由后台。再登录软路由后台系统,检查网络状况,已连通。

]]>
之前刷推看到了不少人发软路由,最近又看到a姐发了一句:全推入手软路由了 开始我还觉得软路由对我的作用应该不大吧,随着从众心理的影响,我觉得我应该试试。 刚好我还有从笔记本上拆下的内存和固态,岂不是严丝合缝?
Clash 设置国内国外自动分流访问 clash-auto-triage 2023-03-10T17:15:55+08:00 2023-03-10T17:15:55+08:00

Clash 是一款开源的网络代理工具,可以帮助用户实现对网络流量的控制和管理。我使用了很久,但苦于每次访问国内外网络需要手动开关代理,于是我就问了下GPT, 还真就解决了。

如果你也需要设置 Clash 区分国内和国外流量,可以按照以下步骤进行操作:

1. 打开配置文件(windows在右下角):

image.png

image.png

使用 Visual Studio Code 打开 config.yaml

内容是这样的:


#---------------------------------------------------#

## 配置文件需要放置在 $HOME/.config/clash/*.yaml



## 这份文件是clashX的基础配置文件,请尽量新建配置文件进行修改。

## !!!只有这份文件的端口设置会随ClashX启动生效



## 如果您不知道如何操作,请参阅 官方Github文档 https://github.com/Dreamacro/clash/blob/dev/README.md

#---------------------------------------------------#



# (HTTP and SOCKS5 in one port)

mixed-port: 7890

# RESTful API for clash

external-controller: 127.0.0.1:9090

allow-lan: false

mode: rule

log-level: warning



proxies: 



proxy-groups:



rules:

- DOMAIN-SUFFIX,google.com,DIRECT

- DOMAIN-KEYWORD,google,DIRECT

- DOMAIN,google.com,DIRECT

- DOMAIN-SUFFIX,ad.com,REJECT

- GEOIP,CN,DIRECT

- MATCH,DIRECT

2. 在 Clash 配置文件中添加以下规则:


# 添加下面的, 跟在后面

payload:

- DOMAIN-SUFFIX,cn,Direct

- DOMAIN-KEYWORD,geosite,Proxy

- IP-CIDR,10.0.0.0/8,Direct

- IP-CIDR,172.16.0.0/12,Direct

- IP-CIDR,192.168.0.0/16,Direct

- IP-CIDR,127.0.0.0/8,Direct

- IP-CIDR,224.0.0.0/4,Direct

- IP-CIDR,240.0.0.0/4,Direct

- MATCH,Final



3. 每个配置段的作用

然后我问了一下 gpt 这些配置的作用:

  • DOMAIN-SUFFIX,cn,Direct 表示所有以 “.cn” 结尾的域名都直接连接(不通过代理)。

  • DOMAIN-KEYWORD,geosite,Proxy 表示包含关键词 “geosite” 的域名(比如 “www.geosite.com”)都通过代理连接。

  • IP-CIDR,10.0.0.0/8,Direct 表示 IP 地址在 10.0.0.0 - 10.255.255.255 范围内的都直接连接。

  • IP-CIDR,172.16.0.0/12,Direct 表示 IP 地址在 172.16.0.0 - 172.31.255.255 范围内的都直接连接。

  • IP-CIDR,192.168.0.0/16,Direct 表示 IP 地址在 192.168.0.0 - 192.168.255.255 范围内的都直接连接。

  • IP-CIDR,127.0.0.0/8,Direct 表示本地回环地址都直接连接(通常是 127.0.0.1)。

  • IP-CIDR,224.0.0.0/4,Direct 和 IP-CIDR,240.0.0.0/4,Direct 表示多播地址都直接连接。

  • MATCH,Final 表示匹配到这条规则后,后面的规则不会再被匹配,可以理解为中断匹配流程,即该规则为终结规则(Final Rule)。

]]>
Clash 是一款开源的网络代理工具,可以帮助用户实现对网络流量的控制和管理。我使用了很久,但苦于每次访问国内外网络需要手动开关代理,于是我就问了下GPT, 还真就解决了。
将本地服务通过 SSH 代理给外部访问 ssh-proxy-conn 2023-03-09T16:08:49+08:00 2023-03-09T16:08:49+08:00

如何使用 ssh 将本地服务代理给外部访问并保持 SSH 会话的连接性

1. 外部服务器 nginx 配置


server {

    listen     localhost:80;

    server_name  _;

    root         /usr/share/nginx/html;



    # 重要:将请求转发到本地服务

    location / {

        root   /usr/share/nginx/html;

        index  index.html index.htm;

        proxy_pass http://127.0.0.1:10412;

        proxy_set_header Host $host:80;

        proxy_set_header X-Real-IP $remote_addr;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header Via "nginx";

    }

}



2. 权限认证

  1. 在外网服务器上运行以下命令以生成公钥:ssh-keygen -o

  2. 将公钥复制到内网服务器上,并添加到 ~/.ssh/authorized_keys

3. 目标内网服务器 ssh 连接

  1. 在本地启动服务并将其监听在端口 8088

  2. 将外网访问的端口 10412 转发到本地端口 8088


nohup ssh -N -v -R 10412:127.0.0.1:8088 root@{外部服务器的外网IP} 2>&1 &

4. 保持会话

  1. 在保持 SSH 会话中,加入以下命令来保持连接

  2. ServerAliveInterval 是指定服务器发送保持连接的数据包的时间(单位:秒)

  3. ServerAliveCountMax 是指定尝试与服务器保持连接的最大次数


nohup ssh -N -v -o ServerAliveInterval=10 -o ServerAliveCountMax=1000 -R 10412:127.0.0.1:8088 root@{外部服务器的外网IP} 2>&1 &

]]>
如何使用 ssh 将本地服务代理给外部访问并保持 SSH 会话的连接性
惊喜又焦虑,AI 技术的发展 the-ai-challenge 2023-03-04T15:48:36+08:00 2023-03-04T15:48:36+08:00

最近,我接触了很多关于AI技术的东西。例如ChatGPT、NewBing、ChatGPT更快的API、stable diffusion、AI语音识别等等。这些技术让我惊喜,也让我感到焦虑。

AI应用日新月异,我的想象力甚至赶不上技术的发展。它们不仅代表了技术的进步,也牵动着就业市场的变革。

我又陷入了意义的怪圈。作为一个软件开发者,我的学习和工作是否还有意义和价值?我的未来会是怎样?如果AI技术可以替代我的工作 ,那么我还能做什么?

如果AI技术只是用于娱乐和教育等领域 ,我或许会感到比较放心,因为我可以成为一个普通的使用者,享受这些技术带来的便利和乐趣,而不必担心它们对我的生活产生过大的影响。

但事实是,AI技术已经渗透到包括软件开发在内各个行业和领域 。AI可以提高软件开发人员的效率和质量 ,也可以拓展软件开发人员的知识和技能 。但同时 ,它们也给软件开发人员带来了更大的竞争压力和更高的要求 。

当然 ,我并不否定AI技术 。它们也有很多优点和潜力 。但是 ,我不能忽视它们带来 的改变和挑战。不知道哪一天,我真的会被AI取代。

后来,我在medium上面看到一篇文章:coding-wont-exist-in-5-years-this-is-why 让我对未来的看法变得理性客观了很多。

AI 驱动的工具将取代人类“编码员”。这些工具将能够比人类更快、更高效地编写和调试代码,而且 成本更低。

如果你所能做的就是写代码,那么你就不是“Engineer”,而是“coder”,你一定会被AI取代。

“幸存下来的不是最强壮的物种,也不是最聪明的物种——它**是最能适应变化的物种。”

*- 查尔斯·达尔文

有了chatGPT这样的工具写代码,只会写代码的人是没有用的。正如工匠能够适应和学习新技能以保持竞争力一样,编码人员将能够通过更多地了解如何使用这些工具来发挥自己的优势来做到这一点。

只有渴望成为房间里最聪明的人,才会担心周围的一切都变得比他聪明。

适应新的方式是痛苦的,但只有活着的人才能感受到这种痛苦——死者甚至感受不到火葬的火光。能利用它的人——将前进,而那些不适应的人将不复存在——it is simple as that.

They Are Coming for You !

]]>
最近,我接触了很多关于AI技术的东西。例如ChatGPT、NewBing、ChatGPT更快的API、stable diffusion、AI语音识别等等。这些技术让我惊喜,也让我感到焦虑。
体验 New Bing:一个比 ChatGPT 更强大、更幽默、更有用的搜索引擎聊天机器人 newbing-vs-chatgpt 2023-03-02T15:43:16+08:00 2023-03-02T15:43:16+08:00

最近在推特上看到很多人在谈论New Bing申请的事情,我也收到了试用通过的邮件。于是我马上打开Edge浏览器体验了一下。

注意:如果你也收到了邀请,但是无法进入聊天页面,请参考这篇文章:https://zhuanlan.zhihu.com/p/605970396

New Bing是一个基于人工智能的搜索引擎聊天机器人。它可以回答各种问题,并且提供相关的链接和信息。它还可以跟你聊天,并且很有幽默感。

我在搜索框输入了一些问题,并且看到了右侧New Bing的回复。我还向上滑动鼠标进入了聊天窗口,在那里我跟New Bing进行了一些对话。其中包括一些荒谬的问题:

6091677668780_.pic_hd.jpg

为了对比New Bing的能力,我还用同样的问题向ChatGPT问了一下。6081677668773_.pic_hd.jpg

从这个问题中可以看出,两个机器人都给出了相对正确的答案。但是New Bing却显得更像一个懂得幽默的人,并且每个专业名词都有引用来源。

以前我曾经问过ChatGPT关于滕王阁序的问题。它告诉我滕王阁序是李白、苏轼、白居易写的。每次问都是不同的答案,但都是错的。今天我又问了同样的问题,它给出了这样的回答:

image.png

现在,它的作者是正确的,但是它的地址、内容描述等等都还是错误的。

同样的问题,bing是怎么回复的呢?

image.png

bing的回复没有任何问题,并且对滕王阁 王勃 骈文 对仗的出处都有标出链接引用。它是从这些答案中汇总得出的。

然后我继续问了几个问题:

image.png

在问到一些想法的问题时,bing的表现明显没有最初好,它在没有引用时,杜撰的内容也是不可信的,比如它说这是滕王阁序的最后一句。

二者 相比,chatGPT的聊天确实是一个Demo级别的产品,而NewBing是一个测试级别的产品。

并且二者在训练的层次和模型版本,应该也有先后之别。

对于普通用户来说,new bing未来应该是更加受欢迎的,加上与Edge的融合,产品体验上应该可以完胜单独的浏览器或者AI Chat。

今天我还听到一个消息,就是GPT的API从3.0升级到了3.5,回复快了数倍,而且还更加便宜。看到hongyi直接拿来接了小爱同学,这两天有时间我也去搞一下子。

]]>
最近在推特上看到很多人在谈论New Bing申请的事情,我也收到了试用通过的邮件。于是我马上打开Edge浏览器体验了一下。
Interesting & Useful 的开源项目 open-source-projects 2023-02-28T15:53:37+08:00 2023-02-28T15:53:37+08:00

| 地址 | 描述 |

| ——————————————————- | ——————————————————————————————————————————————————- |

| https://github.com/lmarzen/esp32-weather-epd | 一个天气显示的墨水瓶项目 |

| https://github.com/AUTOMATIC1111/stable-diffusion-webui | Stable Diffusion 模型的 WebUI 界面。这是一个实现在浏览器上使用的 Stable Diffusion 模型的项目,支持通过文本/图片生成图片、嵌入文本、调整图片大小等功能。 |

| https://github.com/gildas-lormeau/SingleFile | 一键下载网页,能够将网页上的文字、图片等内容,完整地整合到单个 HTML 文件里,支持 Chrome、Firefox、Safari、Microsoft Edge 等主流浏览器。 |

| https://github.com/espanso/espanso | 快捷输入工具 |

| https://github.com/tw93/Pake | 将网页打包成桌面应用,支持Mac Windows Linux |

]]>
分享一些平时遇到的有趣的开源项目
聊天记录存储实践 chat-message-store 2023-02-27T13:53:20+08:00 2023-02-27T13:53:20+08:00

公司的某款游戏在1月初接入微软小冰AI聊天功能。为了保存聊天记录并为后续的统计功能做好准备,决定将聊天记录存放在服务端。最初并不清楚聊天数据量的大小以及玩家对聊天功能的使用情况,所以采用了价格和性能相对宽容的MySQL作为存储介质。

经过大约一个月的运营后,聊天记录表中的数据量已经达到了两千万条。反馈给策划部门后,为控制数据量,决定对聊天记录数量进行限制。每个玩家的每个角色最多只保存200条记录,每个玩家最多可保存3个角色的记录,即每个玩家最多只保存900条聊天记录。服务端的处理逻辑随即修改为每存储300条记录后就清理一次,以确保数据量控制在一定范围内。

根据游戏的新增和玩家留存数据,假设每个玩家平均游戏时间为10天,新增的玩家数在2000至3000之间。因此,每10天会有20000至30000个新的玩家数据,按最高值计算,每10天可能会产生最高2700万条数据,每月最多可能达到5000万条数据。这样计算下来,在一个月后,MySQL可能会无法支持数据的频繁读写操作,需要对聊天记录的存储进行调整。

针对游戏的AVG属性,玩家的生命周期相对较短,因此可以根据玩家的注册时间对其进行划分,从而对玩家数据进行分表存储。具体来说,可以按照玩家注册时间的月份对数据进行划分,并使用相应的表进行数据的存储和查询。这样,每个月份可以相对均匀地承载玩家数据,原本可能达到千万级别的数据量,现在可以控制在百万级别,完全可以解决目前的问题(聊天数据量与新增数据正相关,新增相对稳定)。

接下来需要考虑的是,当一个玩家的生命周期结束后,其数据仍然会一直存储在数据库中。而当新的一年开始,新注册的玩家会继续存储,这样对玩家数据的切片就只是一个临时的措施。解决这个问题的方法是分析玩家在主生命周期结束后的行为,发现这些玩家长时间不会再次登录并进行聊天。因此,可以将这些不活跃的玩家数据迁移到便宜的分布式文件存储中,并且记录迁移标志,删除掉原数据库表中的记录。采用定时任务的方式,这样可以减少对业务的影响。

以上的删除操作,都要注意一点,mysql delete删除操作不会释放表空间。这里需要对表空间进行手动释放。手动释放大表空间是一个比较耗费性能的操作,还会对表数据进行锁定。所以释放操作是需要对实际情况进行设计的。在上面的情况中,我们对数据进行按月操作,可以在一个月后,对上月的数据进行清理。比如 3月初清理1月份的表,因为3月份活跃的玩家大多可能是2月份注册的。可以定在凌晨进行定时删除,对业务影响降到最低。

在2月份,新增了对聊天记录进行标记的操作需求,以进行定期反馈和AI调教。这里我们将标记操作的聊天记录独立出来存储,这样可以方便后续的统计和分析。这样也可以避免将标记操作分散在不同的表中,增加数据处理的复杂性。同时,由于标记操作的数据量不会很大,也可以考虑采用NoSQL数据库或者其他内存型数据库来存储这些数据,以提高查询速度和减少存储成本。

]]>
公司的某款游戏在1月初接入微软小冰AI聊天功能。为了保存聊天记录并为后续的统计功能做好准备,决定将聊天记录存放在服务端。
在 Google 设置静态页面 CDN 加速 google-static-cdn 2023-02-15T16:52:35+08:00 2023-02-15T16:52:35+08:00

在google设置静态页面 CDN加速

一、 创建bucket,设置bucket

https://console.cloud.google.com/storage/browser

  • ##### 创建bucket

image.png

  • ##### 设置bucket公开访问
  1. 在bucket列表中,进入刚创建的bucket。

  2. 选择页面顶部附近的权限标签。

  3. 权限部分中,点击 person_add 授予访问权限按钮。

    此时将显示“授予访问权限”对话框。

  4. 新的主帐号字段中,输入 allUsers

  5. 选择角色下拉列表的过滤条件框中输入 Storage Object Viewer,然后从过滤后的结果中选择 Storage Object Viewer

  6. 点击保存

  7. 点击允许公开访问

二、设置CDN

https://console.cloud.google.com/net-services/cdn/list

添加来源,创建时可能会附带创建好负载均衡,用来做DNS解析使用

image.png

指向刚才新创建的bucket

在host and path rules设置地址规则。

三、DNS解析

进入负载均衡控制台

https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers

选择创建好的负载均衡,点进去,拿到公网IP,到DNS控制台进行解析即可。

]]>
在google设置静态页面 CDN加速
发现优质 Newsletter 和 Blog find-newsletter 2023-02-05T19:47:18+08:00 2023-02-05T19:47:18+08:00

我平时一直都在发现很多优质的资源,但都没有整理。一直增加,单独一个页面可能开始会有些乱,就先开一篇放在这里。

| 名称 | 简介 |

| ——————————————————————————————— | ——————————-|

| 新趣集 | 发现有趣的新产品 |

| AlleyRead | 发现国内外优质内容 |

| 小众软件 - 发现 | 寻找应用、软件 |

| v2ex - 发现 | 分享好玩的,并获取灵感 (须自强) |

| v1tx | 分享实用工具与解决问题 |

| 炒饭 | 发现有趣的内容| |

| 创造者日报 | 每天发现有趣的产品| |

| producthunt | 每天发现有趣的新产品 (国外产品) |

| 找到 AI | 找到你喜爱的作品 |

| 52 破解 | 找到你想要的软件 |

| 发现优质 Newsletter)| 授人予鱼 不如授人予渔, 这是一个newsletter的推荐网站 | |


Copy By yihong Blog , 都是非常好的博客⬇️

@yihong0618 收藏的博客

| 博客名称 | 添加日期 | type | 备注 |

| ——- | ——- | —- | —- |

| niyue | 2022.01.20 | 第一个 | 写 20 年博客的前辈 |

| baotiao | 2022.01.31 | 数据库 | 做有积累的事情 |

| javabin | 2022.02.03 | hacker | 好 cool 的人 |

| soulteary | 2022.02.07 | DIY 神 | 向往 |

| 7dot9 | 2022.02.26 | 前辈 | 梦在这里可以飞翔 |

| chrisdown | 2022.03.05 | SRE | Linux |

]]>
我平时一直都在发现很多优质的资源,但都没有整理。一直增加,单独一个页面可能开始会有些乱,就先开一篇放在这里。
Nginx 代理静态网站 CSS 解析异常 nginx-website 2023-01-31T21:55:01+08:00 2023-01-31T21:55:01+08:00

今天在使用ecs进行部署网页时,出现了一个问题。使用nginx代理到页面index.html路径下,同路径的资源都可以加载到,但是却无法正确加载到页面样式。打开f12,网络和控制台都没有资源异常,但页面乱成了一锅粥。

本地打开是正常的,上到服务器却不行?

之前使用nginx时,并没有这个问题,于是我猜测是不是nginx新的版本对配置参数进行了修改?

但我翻看了nginx的文档,却没有找到。于是我跟着症状开始在网上翻文,终于:

解决方法

若不对于css文件解析进行配置,nginx默认文件都是text/plain类型进行解析,为此我们需要对此进行简单配置。将下面这段代码放入到location模块下面,然后重启nginx。记住一定要清理浏览器的缓存


include mime.types;  

default_type application/octet-stream;

完整如下:


location / {

              include mime.types;

              default_type application/octet-stream;

              alias /opt/website/;

              autoindex on;

         }



]]>
今天在使用ecs进行部署网页时,出现了一个问题。使用nginx代理到页面index.html路径下,同路径的资源都可以加载到,但是却无法正确加载到页面样式。打开f12,网络和控制台都没有资源异常,但页面乱成了一锅粥。 本地打开是正常的,上到服务器却不行?
macOS 12 Monterey + Windows11 我的生产力答案 hackintosh 2022-12-25T21:14:27+08:00 2022-12-25T21:14:27+08:00

大学毕业后,我买了一台乞丐版的 13 寸 MacBook Pro,这台电脑配置比现在的安卓手机还低。今年年中,我开始把用于工作的 MacBook Pro(18 款 15 寸 16+256)带回家使用,虽然勉强能用,但是我渐渐地发现这两台电脑都不能满足我的需求。

从 2020 年开始,我就计划安装一台黑苹果,我把很多零件加入购物车,但是当时显卡实在太贵,所以一直没有开始。后来以太坊价格直线上涨,矿卡价格也水涨船高,苹果公司也开始全面转向 ARM 平台,这使我更加渴望拥有一台强大的“生产力”工具,赶上黑苹果的尾巴。

在我看来,相比苹果设备,黑苹果有以下几个优点:

  1. 价格便宜。

  2. 可以自定义硬件配置,不需要精准的安装技能。

  3. 可以安装 x86 平台的 Windows 系统,可以用于娱乐和生产力。

  4. 探索的过程也是一种快乐。

价格方面,只需要 1000 元左右就可以购买 32GB DDR5 内存,500多块可以买到2TB 固态硬盘等等。


自定义硬件方面,我可以先买一个 32GB 的内存条,然后等到有钱了再买一个(众所都周知,苹果的内存条是金子做的,真买不起)。

同样的道理,我也可以先买一个 5600XT 显卡(甚至是矿卡),然后再换成 6600XT 等更强的显卡。


安装 Windows 是很有必要的,因为无论是娱乐还是工作,Windows 都是必不可少的。

如果你觉得双系统切换麻烦,这个工具了解一下:fa-hand-o-down:

image.png


几个劣势:

  1. 体积大

  2. 兼容性,稳定性差。

  3. 可能无法升级。

  4. 功耗大。

  5. 需要花费一些时间。

在体积上,再怎么取舍,也会远远超过mac stdio的完美集成。我使用的机箱是联立A4 H2O,是比较经典的A4机箱,也要11L的空间。但我这里可以装下一个240水冷,更大的体积也会带来更好的散热。

兼容性的话,在装机时候,需要很多的设置,扒帖,去定制更适配的EFI。

无法升级这个可能存在的问题,我尽量选择新的系统中的稳定的主流的系统,这样,在一次好好准备之后,接近完美的一个系统可以用很久。

功耗,对一个普通台式机玩家,不是问题。

花费时间是可以取舍的,如果想要尽量快,就去尽量找别人准备好的,可能会花费几块钱;如果喜欢折腾,就去扒帖,学习设置。白果只有物流时间。


然后,我进行了一些性能测试。总体来说,我的黑苹果系统表现不错,性能比我之前的MacBook Pro要好得多。我可以运行Adobe Creative Suite和Final Cut Pro等专业软件,没有出现任何问题。

此外,我也可以在我的黑苹果上安装Windows系统,这样我就可以玩一些Windows平台上的游戏了。这是Mac OS系统所不能提供的,这也是我决定装黑苹果的原因之一。

当然,黑苹果也有一些劣势,比如兼容性和稳定性相对较差,可能需要花费一些时间来解决问题。但是,对于我这种喜欢折腾的人来说,这些问题并不是什么大问题。我可以通过网上的资源和社区来解决这些问题。

总的来说,装黑苹果是一项有趣的尝试。虽然过程可能会有些繁琐,但是如果你是一个喜欢尝试新事物和折腾的人,那么我认为你可以尝试一下。:tw-1f603:


然后就是我的整个装机历程了:

零件:


1. 机箱 联立A4 H2O 900RMB

2. 电源 长城tf750 729RMB

3. 主板 ROG B660itx 1250RMB

4. 内存 海盗船DDR5 5200MHZ 1000RMB

5. CPU i512490F盒装 1050RMB

6. 显卡 蓝宝石5600XT 白金版 6G 628RMB

7. 固态 七彩虹2TB 569RMB

8. 水冷 利民冰封幻境240 RGB 300RMB

共: 6426RMB

win11固态出厂预装

在网上找到B660I 主板设置好的EFI文件

下载和这个EFI文件适配的带引导的dmg系统镜像,直接写盘,更换EFI,重启安装。

之后我会把相对应的教程和文件贴一下,如果有同学看到了,感兴趣,着急的可以直接留言,我会尽快整理的。 :tw-1f618:

]]>
大学毕业以后,买了一台乞丐版的13寸的macbook pro(8 + 128 这个配置可能还不如现在的安卓手机),今年年中,我开始把工作用的macbook pro(18款 15寸 16 + 256)往家里带,勉强够用。到了今年下半年,我渐渐感觉,这俩电脑,一个能打的都没有,真顶不住了。
最新版 Let’s Encrypt免费证书申请步骤,保姆级教程 lets-encrypt 2022-12-02T19:17:50+08:00 2022-12-02T19:17:50+08:00

最近将域名迁到了google domain,就研究了一下Let’s Encrypt的域名证书配置。发现网上找到的教程在官方说明中已经废弃,所以自己写一个流程记录一下。

步骤方法官方文档见:https://eff-certbot.readthedocs.io/en/stable/install.html#installation

snapd官方文档见:https://certbot.eff.org/instructions

1. 安装snapd(这里我使用的是centos系统)


   sudo yum install snapd

   sudo systemctl enable --now snapd.socket

   sudo ln -s /var/lib/snapd/snap /snap

2. 使用snapd安装certbot


   sudo snap install --classic certbot

   sudo ln -s /snap/bin/certbot /usr/bin/certbot

####3. 生成证书(需要指定nginx)

:fa-chevron-circle-right: 手动安装nginx


   certbot certonly --nginx --nginx-ctl /usr/local/nginx/sbin/nginx --nginx-server-root /usr/local/nginx/conf

这里的certonly就是只下载对应文件,不进行配置nginx,适用于自己配置或者更新使用。去掉则会帮你进行配置nginx(我没有试用)。

可能出现的问题:

The error was: PluginError(‘Nginx build is missing SSL module (–with-http_ssl_module).’)

提示这个错误是因为目前nginx缺少–with-http_ssl_module这个模块,我们要添加这个模块。重新编译nginx

进入nginx下载的目录

./configure –prefix=/usr/local/nginx –with-http_ssl_module

编译完成后

make

make install

/usr/local/nginx/sbin/nginx -s stop

/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

使用 /usr/local/nginx/sbin/nginx -V 查看是否生效


   [root]# /usr/local/nginx/sbin/nginx -V

   nginx version: nginx/1.23.2

   built by gcc 10.2.1 20200825 (Alibaba 10.2.1-3 2.32) (GCC) 

   built with OpenSSL 1.1.1k  FIPS 25 Mar 2021

   TLS SNI support enabled

   configure arguments: --prefix=/usr/local/nginx --with-http_ssl_module

生效,重新执行上面的命令即可。

4. 生成证书

执行上面的命令后,程序会让你确认你的邮箱和你的域名,确认完成后会将证书文件生成在指定目录中。


   certbot certonly --nginx --nginx-ctl /usr/local/nginx/sbin/nginx --nginx-server-root /usr/local/nginx/conf

   Saving debug log to /var/log/letsencrypt/letsencrypt.log

   Enter email address (used for urgent renewal and security notices)

    (Enter 'c' to cancel): [这里输入你的邮箱]

   

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   Please read the Terms of Service at

   https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf. You must

   agree in order to register with the ACME server. Do you agree?

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   (Y)es/(N)o: Y

   

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   Would you be willing, once your first certificate is successfully issued, to

   share your email address with the Electronic Frontier Foundation, a founding

   partner of the Let's Encrypt project and the non-profit organization that

   develops Certbot? We'd like to send you email about our work encrypting the web,

   EFF news, campaigns, and ways to support digital freedom.

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   (Y)es/(N)o: Y [选Y 继续]

   Account registered.

   

   Which names would you like to activate HTTPS for?

   We recommend selecting either all domains, or all domains in a VirtualHost/server block.

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   1: whrss.com

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   Select the appropriate numbers separated by commas and/or spaces, or leave input

   blank to select all options shown (Enter 'c' to cancel):  [这里不需要输入,回车选所有]

   Requesting a certificate for whrss.com

   

   Successfully received certificate.

   Certificate is saved at: 

   # [这里告诉我们生成的文件路径和有效期]

   /etc/letsencrypt/live/whrss.com/fullchain.pem

   Key is saved at:         /etc/letsencrypt/live/whrss.com/privkey.pem

   This certificate expires on 2023-03-02.

   These files will be updated when the certificate renews.

   Certbot has set up a scheduled task to automatically renew this certificate in the background.

   

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   If you like Certbot, please consider supporting our work by:

    * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate

    * Donating to EFF:                    https://eff.org/donate-le

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   

5. Nginx.conf的配置


   server{

           #监听443端口

           listen 443 ssl;

           #对应的域名,空格分隔域名就可以了

           server_name whrss.com; 

           #第一个域名的文件

           ssl_certificate /etc/letsencrypt/live/whrss.com/fullchain.pem;

           ssl_certificate_key /etc/letsencrypt/live/whrss.com/privkey.pem;

           # 其他配置

           ssl_session_timeout 5m;

           ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

           ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;

           ssl_prefer_server_ciphers on;

           #这是我的主页访问地址,因为使用的是静态的html网页,所以直接使用location就可以完成了。

           location / {

               root   /;

               index  /;

               proxy_pass http://127.0.0.1:9091;

               proxy_set_header Host $host:443;

               proxy_set_header X-Real-IP $remote_addr;

               proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

               proxy_set_header Via "nginx";

           }

   }

以上!!

]]>
最近将域名迁到了google domain,就研究了一下Let’s Encrypt的域名证书配置。发现网上找到的教程在官方说明中已经废弃,所以自己写一个流程记录一下。
五人墓碑记 tombstone-of-the-five 2022-11-28T10:02:25+08:00 2022-11-28T10:02:25+08:00
作者:张溥 明

五人者,盖当蓼洲周公之被逮,激于义而死焉者也。至于今,郡之贤士大夫请于当道,即除逆阉废祠之址以葬之,且立石于其墓之门,以旌其所为。呜呼,亦盛矣哉!夫五人之死,去今之墓而葬焉,其为时止十有一月尔。夫十有一月之中,凡富贵之子,慷慨得志之徒,其疾病而死,死而堙没不足道者,亦已众矣,况草野之无闻者欤?独五人之皦皦,何也?

予犹记周公之被逮,在丁卯三月之望。吾社之行为士先者,为之声义,敛赀财以送其行,哭声震动天地。缇骑按剑而前,问:“谁为哀者?”众不能堪,抶而仆之。是时以大中丞抚吴者,为魏之私人,周公之逮所由使也。吴之民方痛心焉,于是乘其厉声以呵,则噪而相逐,中丞匿于溷藩以免。既而以吴民之乱请于朝,按诛五人,曰颜佩韦、杨念如、马杰、沈扬、周文元,即今之傫然在墓者也。然五人之当刑也,意气阳阳,呼中丞之名而詈之,谈笑以死。断头置城上,颜色不少变。有贤士大夫发五十金,买五人之脰而函之,卒与尸合。故今之墓中,全乎为五人也。

嗟夫!大阉之乱,缙绅而能不易其志者,四海之大,有几人欤?而五人生于编伍之间,素不闻《诗》《书》之训,激昂大义,蹈死不顾,亦曷故哉?且矫诏纷出,钩党之捕遍于天下,卒以吾郡之发愤一击,不敢复有株治。大阉亦逡巡畏义,非常之谋,难于猝发。待圣人之出而投环道路,不可谓非五人之力也。

繇是观之,则今之高爵显位,一旦抵罪,或脱身以逃,不能容于远近,而又有翦发杜门,佯狂不知所之者,其辱人贱行,视五人之死,轻重固何如哉?是以蓼洲 周公,忠义暴于朝廷,赠谥美显,荣于身后。而五人亦得以加其土封,列其姓名于大堤之上,凡四方之士,无有不过而拜且泣者,斯固百世之遇也。不然,令五人者保其首领,以老于户牖之下,则尽其天年,人皆得以隶使之,安能屈豪杰之流,扼腕墓道,发其志士之悲哉?故予与同社诸君子,哀斯墓之徒有其石也,而为之记,亦以明死生之大,匹夫之有重于社稷也。

贤士大夫者,冏卿因之吴公、太史文起文公、孟长姚公也。

]]>
五人墓碑记 明 张溥  卒以吾辈之发愤一击,不敢复有株治。
随想—生活效能 effectiveness 2022-11-27T15:34:45+08:00 2022-11-27T15:34:45+08:00

之前我有写过一篇文章,是关于边际效益的思考的,但很不幸,我没有备份,一并被阿里云ban掉了。

那篇文章是基于李永乐老师的一个视频——996的那个,来讨论的。最近,我发现在生活中,我有不少关于效益的习惯和思考,记录一下。

最近,我意识到自己对于效益的思考和行为有不少习惯,其中一个例子是洗衣服。由于我租住的房子没有洗衣机,而我个人穿着简单,衣服也很少,所以我通常都是手洗。刚开始,我只是随便洗一下,因为感觉这样能省事,但是随着时间的推移,我开始思考这个过程中的效益和成本,并且逐渐调整了我的洗衣方式。

首先,我从心理层面分析了这个问题。我从洗衣液问世时就开始使用它,因为它能轻松地溶解脏污,尤其是一些不易清洗的面料。这种想法让我认为只要用洗衣液泡一下,就能够解决70%左右的污渍。对于日常穿着,这种简单的清洗方式足以满足需求。但是对于有些衣服,比如容易掉色的棉制外裤,洗得太狠会影响寿命,这也是我使用简单方法洗衣服的原因之一。此外,冬天水太冷,而且洗衣机所需的衣服数量太多,所以我不得不简化洗衣的流程。

接着,我开始考虑“边际效益”。我想到了洗衣过程中从“干净”到“脏”的过渡,这个过程需要进行比较,比如,当衣服开始散发出汗味时,就需要清洗。我需要洗衣服来保证我的整体干净程度。在控制洗衣液和水温等变量条件下,衣服的干净程度和所需要付出的劳动基本上可以看作一个效益曲线。这个曲线上有一个最佳点,也就是衣服的干净程度和所付出的劳动量之间的比例最佳。在这个点上,我们付出的劳动和晾晒的时间等等,可以最大程度地轻松达到一个干净的程度。如果要让衣服更加干净,所需付出的劳动就会越来越大。

因此,我选择了一种简单而有效的洗衣方式:浸泡、揉搓、涮净、拧干和晾晒。这个过程几乎不需要花费多少时间和经历,在让衣服变得干净的同时,也会大大延长衣物的寿命。

效能,效就是结果,能就是取效所做的功。在实现目标的过程中,所需付出的劳动与产生的效果之间存在一种平衡点,我们需要找到这个点,以达到最大化效益的目的。

]]>
效能,效就是结果,能就是取效所做的功。
测试一下功能吧 test-function 2022-11-24T21:11:52+08:00 2022-11-24T21:11:52+08:00

阿里服务器被ban了,数据全没了,测试一下重新组织博客能否正常工作吧


全部组件都使用了国外免费的平台。

]]>
阿里服务器被ban了,数据全没了