├── .github └── workflows │ ├── go.yml │ └── lint.yml ├── .gitignore ├── LICENSE ├── Makefile ├── Makefile.cross-compiles ├── README.md ├── build.sh ├── cmd ├── chain_game.go ├── daemon.go ├── games.go ├── gen_model_cmd.go ├── import.go └── phrase.go ├── collector ├── bbdc │ ├── bbdc.go │ └── struct.go ├── collector.go └── file │ └── file.go ├── conf ├── conf.go └── conf.yml ├── core ├── core.go └── option.go ├── dao ├── ctx.go ├── dao.go ├── model │ ├── phrase.gen.go │ ├── phrase.go │ ├── word.gen.go │ ├── word.go │ ├── word_phrase.gen.go │ └── word_phrase.go ├── phrase.go ├── query │ ├── phrase.gen.go │ ├── query.go │ ├── word.gen.go │ └── word_phrase.gen.go ├── word.go └── word_phrase.go ├── db ├── db.go └── sqlite │ ├── helloword.sql │ ├── option.go │ └── settings.go ├── games ├── chain.go └── chain_test.go ├── generator ├── generator.go └── gpt3 │ └── client.go ├── go.mod ├── go.sum ├── library ├── CET4.txt ├── CET6.txt ├── GRE_8000.txt ├── GRE_abridged.txt ├── example.png └── word_chain.png ├── logging ├── helper.go └── log.go ├── main.go ├── notify ├── base │ └── subject.go ├── dingtalk │ └── dingtalk.go ├── lark │ ├── lark.go │ └── struct.go ├── notify.go └── telegram │ └── telegram.go ├── selector ├── options.go ├── selector.go └── strategy │ ├── least_recently_used.go │ └── random.go └── tools ├── fx ├── README.md ├── fn.go ├── fx_test.go └── ring.go ├── helper.go └── restry.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | go: [1.18, 1.19] 17 | name: build & test 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Go 22 | uses: actions/setup-go@v3.3.1 23 | with: 24 | go-version: ${{ matrix.go }} 25 | 26 | - name: Upload coverage to Codecov 27 | run: bash <(curl -s https://codecov.io/bash) 28 | - name: helloword 29 | run: | 30 | go test ./... 31 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # This workflow executes several linters on changed files based on languages used in your code base whenever 2 | # you push a code or open a pull request. 3 | # 4 | # You can adjust the behavior by modifying this file. 5 | # For more information, see: 6 | # https://github.com/github/super-linter 7 | name: Lint Code Base 8 | 9 | on: 10 | push: 11 | branches: [ main ] 12 | pull_request: 13 | branches: [ main ] 14 | jobs: 15 | lint: 16 | name: lint module 17 | runs-on: ubuntu-latest 18 | needs: resolve-modules 19 | strategy: 20 | matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Lint 24 | uses: golangci/golangci-lint-action@v3 25 | with: 26 | version: latest 27 | working-directory: ${{ matrix.workdir }} 28 | skip-pkg-cache: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/.* 2 | .idea 3 | conf/conf.yml 4 | conf/conf.example.yml 5 | 6 | release/ 7 | packages/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Remember 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: checklint 2 | checklint: 3 | ifeq (, $(shell which golangci-lint)) 4 | @echo 'error: golangci-lint is not installed, please exec `brew install golangci-lint`' 5 | @exit 1 6 | endif 7 | 8 | .PHONY: lint 9 | lint: checklint 10 | golangci-lint run --skip-dirs-use-default 11 | 12 | 13 | win: 14 | GOOS=windows GOARCH=amd64 go build -o helloword-windows-amd64.exe main.go 15 | macos-adm64: 16 | GOOS=darwin GOARCH=amd64 go build -o helloword-macos-amd64 main.go 17 | linux: 18 | GOOS=linux GOARCH=amd64 go build -o helloword-linux-amd64 main.go 19 | macos-arm64: 20 | GOOS=darwin GOARCH=arm64 go build -o helloword-macos-arm64 main.go 21 | 22 | build: win macos-adm64 linux macos-arm64 23 | 24 | 25 | all: 26 | $(shell sh build.sh) -------------------------------------------------------------------------------- /Makefile.cross-compiles: -------------------------------------------------------------------------------- 1 | export PATH := $(GOPATH)/bin:$(PATH) 2 | export GO111MODULE=on 3 | LDFLAGS := -s -w 4 | 5 | os-archs=darwin:amd64 darwin:arm64 linux:amd64 linux:arm64 windows:amd64 windows:arm64 6 | 7 | all: build 8 | 9 | build: app 10 | 11 | app: 12 | @$(foreach n, $(os-archs),\ 13 | os=$(shell echo "$(n)" | cut -d : -f 1);\ 14 | arch=$(shell echo "$(n)" | cut -d : -f 2);\ 15 | gomips=$(shell echo "$(n)" | cut -d : -f 3);\ 16 | target_suffix=$${os}_$${arch};\ 17 | echo "Build $${os}-$${arch}...";\ 18 | GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/helloword_$${target_suffix} main.go;\ 19 | echo "Build $${os}-$${arch} done";\ 20 | ) 21 | @mv ./release/helloword_windows_amd64 ./release/helloword_windows_amd64.exe 22 | @mv ./release/helloword_windows_arm64 ./release/helloword_windows_arm64.exe 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hello word 2 | 3 | ### 背景 4 | 5 | Hello 6 | Word是我在背单词的过程中想到的一个想法。在学习英语时,词汇量是非常重要的。仅仅死记硬背单词,没有语境感,效率是很低的。虽然一些应用程序可以根据单词的多个词义为单词组成一小段句子,稍微增强语境感。但是单词仍然过于零散。因此,我们是否可以将每天背诵的多个单词组合成一段小短文,以便复习这一批单词呢?这就是Hello 7 | Word的初衷。当然,ChatGPT API是实现这个想法的工具。 8 | 除此之外,还配套了几个周边小游戏。 9 | 10 | ### 初始化 11 | 12 | #### 环境变量 13 | 14 | ```shell 15 | HELLO_WORD_PATH # 默认不配置在 ~/.helloword 16 | ``` 17 | 18 | #### 配置文件 19 | 20 | 配置文件在conf/conf.yml里。 21 | 22 | ```yaml 23 | gptToken: "xx" #要使用短语推送功能这个是必须的!!!可以先注册openai,然后到platform.openai.com平台申请key 24 | 25 | collector: 26 | bbdc: # 不背单词cookie,配置了可以同步不背单词的生词表。 27 | cookie: 28 | 29 | # --------------------- Notification Configuration --------------------- 30 | notify: # 通知配置,目前支持telegram,dingtalk,lark可以全配,那么所有平台都会推送一遍 31 | # telegram: 32 | # token: xxxxxxx # Bot Token 33 | # chat_id: -123456789 # Channel / Group ID 34 | # dingtalk: 35 | # webhook: "https://oapi.dingtalk.com/robot/send?access_token=xxxx" 36 | # secret: "" # sign secret if set 37 | lark: 38 | webhook: "xx" 39 | ``` 40 | 41 | #### 代理 42 | 43 | 由于众所周知的原因,所以你可能需要代理, 44 | 环境变量: 45 | 46 | ```dotenv 47 | # socks5或者http 48 | # etc. 49 | PROXY_URI=socks5://ip:port 50 | ``` 51 | 52 | ### 单词短语推送器 53 | 54 | 指定单词数量,随机选择单词,生成一段小短文,推送到用户指定平台。 55 | 56 | ```shell 57 | ./helloword daemon --files="CET4.txt,CET6.txt" --spec="@every 10s" --word-number=8 --c=conf.yml 58 | ``` 59 | 60 | **参数说明** 61 | 这个程序有以下可选项: 62 | 63 | - files:默认导入 CET4.txt 单词文件,你可以通过逗号同时导入多个单词文件,它们都存储在 library 文件夹下。 64 | - spec:表示推送频率设置,默认为每小时生成一个新的短语,具体时间规则使用的是 [robif/cron](https://github.com/robfig/cron) 65 | 库,请参考该库的文档自行设置。 66 | - word-number:表示生成一次短语使用的单词数量,默认为 5 个,最多不超过 10 个 67 | - strategy: 单词选择策略,默认随机random,还可选择 leastRecentlyUsed,最近最少被使用的单词 68 | - conf: 可选,配置文件。具体配置信息如上 69 | 70 | ![example](./library/example.png) 71 | 72 | **单词选择规则** 73 | 74 | - 默认:随机 (done) 75 | - 最近最少推送(done) 76 | 77 | ### 指定单词,直接生成短语 78 | 79 | ```shell 80 | ./helloword phrase "approach,proportion,academy,weapon" 81 | ``` 82 | 83 | ### 单词游戏 84 | 85 | #### 单词接龙 86 | 87 | **游戏规则** 88 | 这是一个单词接龙游戏,游戏开始时系统会随机选择一个单词。玩家需要以该单词的最后一个字母为开头输入一个新单词,接着程序又以玩家输入单词的最后一个字母为开头输出新单词。游戏会持续进行,直到有一方出现错误。在一局游戏中,每个单词只能被使用一次。 89 | 90 | 使用 91 | 92 | ```shell 93 | ./helloword games chain --files="CET4.txt,CET6.txt" 94 | ``` 95 | 96 | **参数说明** 97 | 98 | - files 可选,如上 99 | - timeout 可选,每一轮超过时间未答题游戏会结束,超时时间默认十秒 100 | 101 | ![example](./library/word_chain.png) 102 | 103 | **后续规划** 104 | 105 | - 单词正确性校验,是否是合法的英语单词(todo) 106 | - 超时控制,用户每个回合指定时间内未输出,游戏结束(done) 107 | 108 | #### 单词拼写(todo) 109 | 110 | #### 单词填空(todo) 111 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | # cross_compiles 4 | make -f ./Makefile.cross-compiles 5 | rm -rf ./release/packages 6 | mkdir -p ./release/packages 7 | 8 | os_all='linux windows darwin' 9 | arch_all='amd64 arm64' 10 | 11 | cd ./release 12 | 13 | for os in $os_all; do 14 | for arch in $arch_all; do 15 | hello_dir_name="helloword_${os}_${arch}" 16 | hello_path="./packages/helloword_${os}_${arch}" 17 | 18 | if [ "x${os}" = x"windows" ]; then 19 | if [ ! -f "./helloword_${os}_${arch}.exe" ]; then 20 | continue 21 | fi 22 | mkdir ${hello_path} 23 | mv ./helloword_${os}_${arch}.exe ${hello_path}/helloword.exe 24 | else 25 | if [ ! -f "./helloword_${os}_${arch}" ]; then 26 | continue 27 | fi 28 | mkdir ${hello_path} 29 | mv ./helloword_${os}_${arch} ${hello_path}/helloword 30 | fi 31 | cp ../LICENSE ${hello_path} 32 | cp ../library/* ${hello_path} 33 | cp ../conf/conf.yml ${hello_path}/conf.yml 34 | 35 | # packages 36 | cd ./packages 37 | if [ "x${os}" = x"windows" ]; then 38 | zip -rq ${hello_dir_name}.zip ${hello_dir_name} 39 | else 40 | tar -zcf ${hello_dir_name}.tar.gz ${hello_dir_name} 41 | fi 42 | cd .. 43 | rm -rf ${hello_path} 44 | done 45 | done 46 | -------------------------------------------------------------------------------- /cmd/chain_game.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/urfave/cli/v2" 12 | "github.com/wuqinqiang/helloword/collector" 13 | "github.com/wuqinqiang/helloword/collector/file" 14 | "github.com/wuqinqiang/helloword/dao" 15 | "github.com/wuqinqiang/helloword/games" 16 | ) 17 | 18 | var wordChainCmd = &cli.Command{ 19 | Name: "chain", 20 | Flags: []cli.Flag{ 21 | &cli.StringFlag{ 22 | Name: "files", 23 | Usage: "传入要导入的Library目录下的单词文件。例如你可以导入一个文件 damon --files=CET4.txt" + 24 | "或者多个文件用逗号隔开,damon --files=CET4.txt,CET6.txt", 25 | }, 26 | &cli.Int64Flag{ 27 | Name: "timeout", //unit of second 28 | Usage: "超时时间,每轮用户超时没回答游戏结束", 29 | Value: 10, // default 10 seconds 30 | }, 31 | }, 32 | Before: func(cctx *cli.Context) error { 33 | var collectors []collector.Collector 34 | files := "CET4.txt" 35 | if cctx.String("files") != "" { 36 | files = cctx.String("files") 37 | } 38 | collectors = append(collectors, file.New(files)) 39 | importer := collector.NewImporter(collectors...) 40 | 41 | return importer.Import(cctx.Context) 42 | }, 43 | Action: func(cctx *cli.Context) error { 44 | timeout := cctx.Int64("timeout") 45 | if timeout <= 0 { 46 | timeout = 10 47 | } 48 | 49 | list, err := dao.NewWord().GetList(cctx.Context) 50 | if err != nil { 51 | return err 52 | } 53 | if len(list) == 0 { 54 | return errors.New("please import some word data first") 55 | } 56 | startWord := list.RandomPick() 57 | chain := games.NewWordChain(list, startWord) 58 | 59 | fmt.Println("Game start") 60 | fmt.Println("Start word:", chain.StartWord().Word, " ", chain.StartWord().Definition) 61 | 62 | timeoutDuration := time.Duration(timeout) * time.Second 63 | timer := time.AfterFunc(timeoutDuration, func() { 64 | fmt.Println("\nTime's up. Game over!") 65 | os.Exit(0) 66 | }) 67 | defer timer.Stop() 68 | 69 | scanner := bufio.NewScanner(os.Stdin) 70 | for { 71 | fmt.Print("> ") 72 | if !scanner.Scan() { 73 | break 74 | } 75 | timer.Reset(timeoutDuration) 76 | 77 | word := scanner.Text() 78 | if word == "" { 79 | fmt.Println("Invalid word. Game over!") 80 | break 81 | } 82 | if word == "exit" { 83 | break 84 | } 85 | 86 | if !strings.HasSuffix(chain.PrevWord().Word, word[0:1]) { 87 | fmt.Println("Invalid word. Game over!") 88 | break 89 | } 90 | 91 | if !chain.SetPrevWord(word) { 92 | fmt.Println("the word has already been used. Game over!") 93 | break 94 | } 95 | 96 | nextWord, ok := chain.Pick() 97 | if !ok { 98 | fmt.Println("Congratulations, you win!") 99 | break 100 | } 101 | fmt.Println("Next word:", nextWord.Word, " ", nextWord.Definition) 102 | } 103 | return nil 104 | }, 105 | } 106 | -------------------------------------------------------------------------------- /cmd/daemon.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/wuqinqiang/helloword/collector/file" 5 | 6 | "github.com/wuqinqiang/helloword/collector/bbdc" 7 | 8 | "github.com/urfave/cli/v2" 9 | "github.com/wuqinqiang/helloword/collector" 10 | "github.com/wuqinqiang/helloword/conf" 11 | "github.com/wuqinqiang/helloword/core" 12 | "github.com/wuqinqiang/helloword/generator/gpt3" 13 | "github.com/wuqinqiang/helloword/notify" 14 | "github.com/wuqinqiang/helloword/selector" 15 | ) 16 | 17 | var DaemonCmd = &cli.Command{ 18 | Name: "daemon", 19 | Usage: "daemon", 20 | Flags: []cli.Flag{ 21 | &cli.StringFlag{ 22 | Name: "files", 23 | Usage: "传入要导入的Library目录下的单词文件。例如你可以导入一个文件 damon --files=CET4.txt" + 24 | "或者多个文件用逗号隔开,damon --files=CET4.txt,CET6.txt", 25 | }, 26 | &cli.StringFlag{ 27 | Name: "spec", 28 | Usage: "推送时间频率控制,默认1小时推送一次短语。自定义比如每5分钟推送一次: @every 5m。" + 29 | "其他规则参考库github.com/robfig/cron", 30 | }, 31 | &cli.IntFlag{ 32 | Name: "word-number", 33 | Usage: "多少个单词组成一个短语", 34 | Value: 5, //default 35 | }, 36 | &cli.StringFlag{ 37 | Name: "strategy", 38 | Usage: "单词选择策略,默认随机random,还可选择 leastRecentlyUsed,最近最少被使用的单词", 39 | Value: "random", 40 | }, 41 | &cli.StringFlag{ 42 | Name: "conf", 43 | Aliases: []string{"c"}, 44 | }, 45 | &cli.StringFlag{ 46 | Name: "bbdc-cookie", 47 | EnvVars: []string{"BBDC_COOKIE"}, 48 | }, 49 | &cli.StringFlag{ 50 | Name: "proxy-url", 51 | EnvVars: []string{"PROXY_URI"}, 52 | }, 53 | }, 54 | 55 | Action: func(cctx *cli.Context) error { 56 | settings, err := conf.GetConf(cctx.String("conf")) 57 | if err != nil { 58 | return err 59 | } 60 | generator, err := gpt3.NewClient(settings.GptToken, cctx.String("proxy-url")) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | var collectors []collector.Collector 66 | 67 | bbcdCookie := cctx.String("bbdc-cookie") 68 | if bbcdCookie != "" { 69 | collectors = append(collectors, bbdc.New(bbcdCookie)) 70 | } 71 | 72 | files := "CET4.txt" 73 | if cctx.String("files") != "" { 74 | files = cctx.String("files") 75 | } 76 | collectors = append(collectors, file.New(files)) 77 | importer := collector.NewImporter(collectors...) 78 | 79 | strategy := selector.Random 80 | if cctx.String("strategy") == string(selector.LeastRecentlyUsed) { 81 | strategy = selector.LeastRecentlyUsed 82 | } 83 | s := selector.New(strategy, selector.WithWordNumber(cctx.Int("word-number"))) 84 | 85 | n := notify.New(settings.Senders()) 86 | core := core.New(generator, importer, s, n, core.WithSpec(cctx.String("spec"))) 87 | 88 | return core.Run() 89 | }, 90 | } 91 | -------------------------------------------------------------------------------- /cmd/games.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | var GamesCmd = &cli.Command{ 8 | Name: "games", 9 | Usage: "import your own words", 10 | Subcommands: []*cli.Command{ 11 | wordChainCmd, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /cmd/gen_model_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | "github.com/wuqinqiang/helloword/dao/model" 6 | "github.com/wuqinqiang/helloword/db" 7 | "gorm.io/gen" 8 | ) 9 | 10 | var GenCmd = &cli.Command{ 11 | Name: "gen", 12 | Usage: "offline deals", 13 | Action: func(context *cli.Context) error { 14 | // specify the output directory (default: "./query") 15 | // ### if you want to query without context constrain, set mode gen.WithoutContext ### 16 | outPath := "dao/query" 17 | g := gen.NewGenerator(gen.Config{ 18 | OutPath: outPath, 19 | OutFile: outPath + "/query.go", 20 | /* Mode: gen.WithoutContext|gen.WithDefaultQuery*/ 21 | //if you want the nullable field generation property to be pointer type, set FieldNullable true 22 | /* FieldNullable: true,*/ 23 | //If you need to generate index tags from the database, set FieldWithIndexTag true 24 | /* FieldWithIndexTag: true,*/ 25 | }) 26 | 27 | // reuse the database connection in Project or create a connection here 28 | // if you want to use GenerateModel/GenerateModelAs, UseDB is necessary or it will panic 29 | //db, err := gorm.Open(mysql.Open("root:root@tcp(127.0.0.1:3306)/boost?charset=utf8&parseTime=True&loc=Local")) 30 | g.UseDB(db.Get()) 31 | g.GenerateAllTable() 32 | g.ApplyBasic(model.Word{}, model.WordPhrase{}, model.Phrase{}) 33 | g.Execute() 34 | return nil 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /cmd/import.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/urfave/cli/v2" 4 | 5 | // ImportCmd todo import your own words 6 | var ImportCmd = &cli.Command{ 7 | Name: "import", 8 | Usage: "import your own words", 9 | Action: func(context *cli.Context) error { 10 | return nil 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /cmd/phrase.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/wuqinqiang/helloword/logging" 8 | 9 | "github.com/wuqinqiang/helloword/notify/base" 10 | 11 | "github.com/wuqinqiang/helloword/notify" 12 | 13 | "github.com/urfave/cli/v2" 14 | "github.com/wuqinqiang/helloword/conf" 15 | "github.com/wuqinqiang/helloword/generator/gpt3" 16 | ) 17 | 18 | var PhraseCmd = &cli.Command{ 19 | Name: "phrase", 20 | Usage: "Generate phrases directly", 21 | Flags: []cli.Flag{ 22 | &cli.StringFlag{ 23 | Name: "conf", 24 | Aliases: []string{"c"}, 25 | }, 26 | &cli.StringFlag{ 27 | Name: "proxy-url", 28 | EnvVars: []string{"PROXY_URL"}, 29 | }, 30 | }, 31 | Action: func(cctx *cli.Context) error { 32 | req := cctx.Args().Get(0) 33 | if req == "" { 34 | return errors.New("please input your words") 35 | } 36 | conf, err := conf.GetConf(cctx.String("c")) 37 | if err != nil { 38 | return err 39 | } 40 | client, err := gpt3.NewClient(conf.GptToken, cctx.String("proxy-url")) 41 | if err != nil { 42 | return err 43 | } 44 | phrase, err := client.Generate(cctx.Context, strings.Split(req, ",")) 45 | if err != nil { 46 | return err 47 | } 48 | n := notify.New(conf.Senders()) 49 | n.Notify(base.New("", phrase)) 50 | 51 | n.Wait() 52 | logging.Infof("Successfully generated a short phrase") 53 | return nil 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /collector/bbdc/bbdc.go: -------------------------------------------------------------------------------- 1 | package bbdc 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/wuqinqiang/helloword/logging" 9 | 10 | "github.com/wuqinqiang/helloword/tools/fx" 11 | 12 | "github.com/wuqinqiang/helloword/dao/model" 13 | 14 | . "github.com/wuqinqiang/helloword/tools" 15 | ) 16 | 17 | var newWordsURI = "https://bbdc.cn/api/user-new-word" 18 | 19 | type BBDC struct { 20 | cookie string 21 | } 22 | 23 | func New(cookie string) *BBDC { 24 | return &BBDC{ 25 | cookie: cookie, 26 | } 27 | } 28 | 29 | func (b *BBDC) Name() string { 30 | return "BBDC" // 不背单词啦 31 | } 32 | 33 | func (b *BBDC) Collect(ctx context.Context) (words model.Words, err error) { 34 | //get the first page 35 | resp, err := b.request(ctx, 0) 36 | if err != nil { 37 | return nil, err 38 | } 39 | if err = resp.Ok(); err != nil { 40 | return 41 | } 42 | 43 | words = append(words, resp.GetWords()...) 44 | 45 | // end of page 46 | if resp.End() { 47 | return 48 | } 49 | 50 | // total pagesize 51 | totalPage := resp.TotalPage() 52 | 53 | fx.From(func(source chan<- interface{}) { 54 | for i := 1; i < totalPage; i++ { 55 | source <- i 56 | } 57 | }).Walk(func(item interface{}, pipe chan<- interface{}) { 58 | resp, err := b.request(ctx, item.(int)) 59 | if err != nil { 60 | logging.Errorf("[BBDC] request page:%d,err:%v", item.(int), err) 61 | return 62 | } 63 | 64 | if err = resp.Ok(); err != nil { 65 | logging.Errorf("[BBDC] request page errmsg :%d,err:%v", item.(int), err) 66 | return 67 | } 68 | 69 | pipe <- resp.GetWords() 70 | }).ForEach(func(item interface{}) { 71 | words = append(words, item.(model.Words)...) 72 | }) 73 | 74 | return 75 | } 76 | 77 | func (b *BBDC) request(ctx context.Context, page int) (resp *Response, err error) { 78 | now := time.Now() 79 | resp = new(Response) 80 | _, err = Resty.R().SetHeader("Cookie", b.cookie). 81 | SetHeader("Accept", "application/json"). 82 | SetQueryParam("page", strconv.Itoa(page)). 83 | SetQueryParam("time", strconv.Itoa(int(now.Unix()))). 84 | SetResult(&resp). 85 | Get(newWordsURI) 86 | return 87 | } 88 | -------------------------------------------------------------------------------- /collector/bbdc/struct.go: -------------------------------------------------------------------------------- 1 | package bbdc 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/wuqinqiang/helloword/dao/model" 7 | ) 8 | 9 | type Response struct { 10 | ResultCode int `json:"result_code"` 11 | DataKind string `json:"data_kind"` 12 | DataVersion string `json:"data_version"` 13 | ErrorBody struct { 14 | UserMessage string `json:"user_message"` 15 | Info string `json:"info"` 16 | } `json:"error_body"` 17 | 18 | DataBody struct { 19 | WordList []struct { 20 | SentenceList []struct { 21 | Id float64 `json:"id"` 22 | Word string `json:"word"` 23 | WordOriginal string `json:"wordOriginal"` 24 | OriginalContext string `json:"originalContext"` 25 | TranslationContext string `json:"translationContext"` 26 | WordLength float64 `json:"wordLength"` 27 | WordNum float64 `json:"wordNum"` 28 | CourseId string `json:"courseId"` 29 | SentenceId string `json:"sentenceId"` 30 | Url string `json:"url"` 31 | SortLevel float64 `json:"sortLevel"` 32 | SortBy float64 `json:"sortBy"` 33 | } `json:"sentenceList"` 34 | Interpret string `json:"interpret"` 35 | Ukpron string `json:"ukpron"` 36 | Updatetime string `json:"updatetime"` 37 | Word string `json:"word"` 38 | Uspron string `json:"uspron"` 39 | } `json:"wordList"` 40 | PageInfo struct { 41 | TotalRecord float64 `json:"totalRecord"` 42 | PageSize float64 `json:"pageSize"` 43 | TotalPage float64 `json:"totalPage"` 44 | CurrentPage float64 `json:"currentPage"` 45 | } `json:"pageInfo"` 46 | } `json:"data_body"` 47 | } 48 | 49 | func (resp *Response) Ok() error { 50 | if resp.ResultCode == 200 { 51 | return nil 52 | } 53 | return errors.New(resp.ErrorBody.UserMessage) 54 | } 55 | 56 | func (resp *Response) End() bool { 57 | return resp.DataBody.PageInfo.CurrentPage == resp.DataBody.PageInfo.TotalPage 58 | } 59 | 60 | func (resp *Response) TotalPage() int { 61 | return int(resp.DataBody.PageInfo.TotalPage) 62 | } 63 | 64 | func (resp *Response) GetWords() (words model.Words) { 65 | items := resp.DataBody.WordList 66 | if len(items) == 0 { 67 | return 68 | } 69 | for _, item := range items { 70 | word := model.NewWord(item.Word) 71 | word.SetDefinition(item.Interpret) 72 | word.SetPhonetic(item.Ukpron) 73 | words = append(words, word) 74 | } 75 | return 76 | } 77 | -------------------------------------------------------------------------------- /collector/collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/wuqinqiang/helloword/dao" 8 | 9 | "github.com/wuqinqiang/helloword/logging" 10 | 11 | "github.com/wuqinqiang/helloword/dao/model" 12 | ) 13 | 14 | type Collector interface { 15 | Name() string 16 | Collect(ctx context.Context) (model.Words, error) 17 | } 18 | 19 | type Importer struct { 20 | dao.Dao 21 | collectors []Collector 22 | } 23 | 24 | func NewImporter(collectors ...Collector) *Importer { 25 | return &Importer{ 26 | Dao: dao.Get(), 27 | collectors: collectors, 28 | } 29 | } 30 | 31 | func (importer Importer) Import(ctx context.Context) error { 32 | var wg sync.WaitGroup 33 | 34 | for i := range importer.collectors { 35 | wg.Add(1) 36 | 37 | go func(collector Collector) { 38 | defer wg.Done() 39 | words, err := collector.Collect(ctx) 40 | if err != nil { 41 | logging.Errorf("[Import] collect:%s err:%v", collector.Name(), err) 42 | return 43 | } 44 | 45 | if err = importer.Word.BatchInsert(ctx, words); err != nil { 46 | logging.Errorf("[Import] BatchInsert err:%v", err) 47 | } 48 | 49 | }(importer.collectors[i]) 50 | } 51 | 52 | wg.Wait() 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /collector/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "os" 7 | "strings" 8 | 9 | "github.com/wuqinqiang/helloword/logging" 10 | 11 | "github.com/wuqinqiang/helloword/tools/fx" 12 | 13 | "github.com/wuqinqiang/helloword/dao/model" 14 | ) 15 | 16 | type File struct { 17 | list []string 18 | } 19 | 20 | func New(files string) *File { 21 | file := new(File) 22 | file.list = append(file.list, strings.Split(files, ",")...) 23 | return file 24 | } 25 | 26 | func (f *File) Name() string { 27 | return "file" 28 | } 29 | 30 | func (f *File) Collect(ctx context.Context) (model.Words, error) { 31 | var words model.Words 32 | 33 | fx.From(func(source chan<- interface{}) { 34 | for _, file := range f.list { 35 | source <- file 36 | } 37 | 38 | }).Walk(func(item interface{}, pipe chan<- interface{}) { 39 | file, err := os.Open(item.(string)) 40 | if err != nil { 41 | logging.Errorf("Open file:%s err:%v", item.(string), err) 42 | return 43 | } 44 | defer file.Close() //nolint 45 | 46 | scanner := bufio.NewScanner(file) 47 | scanner.Split(bufio.ScanLines) 48 | 49 | var tmp model.Words 50 | for scanner.Scan() { 51 | // etc.wrap [ræp] vt.裹,包,捆 n.披肩 52 | items := strings.Split(scanner.Text(), " ") 53 | if len(items) < 3 { 54 | continue 55 | } 56 | word := model.NewWord(items[0]) 57 | word.SetPhonetic(items[1]) 58 | word.SetDefinition(items[2]) 59 | tmp = append(tmp, word) 60 | } 61 | if len(tmp) <= 0 { 62 | return 63 | } 64 | pipe <- tmp 65 | 66 | }).ForEach(func(item interface{}) { 67 | words = append(words, item.(model.Words)...) 68 | }) 69 | 70 | return words, nil 71 | } 72 | -------------------------------------------------------------------------------- /conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | _ "embed" 5 | "io" 6 | "os" 7 | 8 | "gopkg.in/yaml.v3" 9 | 10 | "github.com/wuqinqiang/helloword/notify" 11 | "github.com/wuqinqiang/helloword/notify/dingtalk" 12 | "github.com/wuqinqiang/helloword/notify/lark" 13 | "github.com/wuqinqiang/helloword/notify/telegram" 14 | ) 15 | 16 | //go:embed conf.yml 17 | var base []byte 18 | 19 | func GetConf(filePath string) (*Settings, error) { 20 | var ( 21 | settings Settings 22 | b = base 23 | ) 24 | 25 | if filePath != "" { 26 | file, err := os.Open(filePath) 27 | if err != nil { 28 | return nil, err 29 | } 30 | defer file.Close() //nolint 31 | 32 | if b, err = io.ReadAll(file); err != nil { 33 | return nil, err 34 | } 35 | } 36 | 37 | err := yaml.Unmarshal(b, &settings) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return &settings, nil 42 | } 43 | 44 | type Settings struct { 45 | GptToken string `yaml:"gptToken"` 46 | Notify 47 | } 48 | 49 | // Notify Config 50 | type Notify struct { 51 | Lark lark.NotifyConfig `yaml:"lark"` 52 | Tg telegram.NotifyConfig `yaml:"tg"` 53 | Dingtalk dingtalk.NotifyConfig `yaml:"dingtalk"` 54 | } 55 | 56 | func (n *Notify) Senders() (senders []notify.Sender) { 57 | if n.Tg.Token != "" && n.Tg.ChatID != "" { 58 | senders = append(senders, n.Tg) 59 | } 60 | if n.Lark.WebhookURL != "" { 61 | senders = append(senders, n.Lark) 62 | } 63 | if n.Dingtalk.SignSecret != "" && n.Dingtalk.WebhookURL != "" { 64 | senders = append(senders, n.Dingtalk) 65 | } 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /conf/conf.yml: -------------------------------------------------------------------------------- 1 | gptToken: "xxx" 2 | 3 | collector: 4 | bbdc: # 不背单词 5 | cookie: 6 | 7 | # --------------------- Notification Configuration --------------------- 8 | notify: 9 | # telegram: 10 | # token: xxxxxxx # Bot Token 11 | # chat_id: -123456789 # Channel / Group ID 12 | # dingtalk: 13 | # webhook: "https://oapi.dingtalk.com/robot/send?access_token=xxxx" 14 | # secret: "" # sign secret if set 15 | lark: 16 | webhook: "xx" -------------------------------------------------------------------------------- /core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/wuqinqiang/helloword/dao" 12 | "github.com/wuqinqiang/helloword/dao/model" 13 | 14 | "github.com/wuqinqiang/helloword/notify/base" 15 | 16 | "github.com/wuqinqiang/helloword/logging" 17 | 18 | "github.com/wuqinqiang/helloword/selector" 19 | 20 | "github.com/wuqinqiang/helloword/tools" 21 | 22 | "github.com/wuqinqiang/helloword/collector" 23 | 24 | "github.com/robfig/cron/v3" 25 | "github.com/wuqinqiang/helloword/generator" 26 | "github.com/wuqinqiang/helloword/notify" 27 | ) 28 | 29 | type Core struct { 30 | dao dao.Dao 31 | 32 | generator generator.Generator 33 | notify notify.Notify 34 | cron *cron.Cron 35 | importer *collector.Importer 36 | selector selector.Selector 37 | ch chan struct{} 38 | locker sync.Mutex 39 | 40 | options *Options 41 | } 42 | 43 | func New(generator generator.Generator, importer *collector.Importer, 44 | selector selector.Selector, notify notify.Notify, opts ...Option) *Core { 45 | core := &Core{ 46 | dao: dao.Get(), 47 | generator: generator, 48 | notify: notify, 49 | options: Default, 50 | cron: cron.New(), 51 | importer: importer, 52 | selector: selector, 53 | ch: make(chan struct{}), 54 | } 55 | 56 | for _, opt := range opts { 57 | opt(core.options) 58 | } 59 | 60 | return core 61 | } 62 | 63 | func (core *Core) Run() error { 64 | defer core.cron.Stop() 65 | 66 | tools.GoSafe(func() { 67 | core.runImport() 68 | }) 69 | 70 | // generatePhrase 71 | if _, err := core.cron.AddFunc(core.options.spec, core.generatePhrase); err != nil { 72 | return err 73 | } 74 | core.cron.Start() 75 | logging.Infof("Hello Word") 76 | ch := make(chan os.Signal, 1) 77 | signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT) 78 | <-ch 79 | return nil 80 | } 81 | 82 | func (core *Core) runImport() { 83 | _ = core.importer.Import(context.Background()) 84 | } 85 | 86 | func (core *Core) generatePhrase() { 87 | defer core.locker.Unlock() 88 | core.locker.Lock() 89 | 90 | parent := context.Background() 91 | ctx, cancel := context.WithTimeout(parent, 92 | time.Duration(core.options.selectTimeout)*time.Second) 93 | defer cancel() 94 | 95 | words, err := core.selector.NextWords(ctx) 96 | if err != nil { 97 | logging.Errorf("[NextWords] err:%v", err) 98 | return 99 | } 100 | if len(words) == 0 { 101 | logging.Warnf("no words available") 102 | return 103 | } 104 | phrase, err := core.generator.Generate(context.Background(), words.List()) 105 | if err != nil { 106 | logging.Errorf("Generate err:%v", err) 107 | return 108 | } 109 | tools.GoSafe(func() { 110 | core.notify.Notify(base.New("本次短语", phrase)) 111 | }) 112 | 113 | core.afterGenerate(phrase, words) 114 | } 115 | 116 | func (core *Core) afterGenerate(phrase string, words model.Words) { 117 | ctx := context.Background() 118 | phraseRecord := model.NewPhrase(phrase) 119 | if err := core.dao.Phrase.Create(ctx, phraseRecord); err != nil { 120 | logging.Errorf("Create Phrase err:%v", err) 121 | return 122 | } 123 | if err := core.dao.WordPhrase.BatchInsert(ctx, 124 | words.GenerateWordPhrase(phraseRecord.PhraseID)); err != nil { 125 | logging.Errorf("WordPhrase BatchInsert err:%v", err) 126 | return 127 | } 128 | if err := core.dao.Word.IncrNumRepetitions(ctx, words.WordIds()); err != nil { 129 | logging.Errorf("IncrNumRepetitions err:%v", err) 130 | return 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /core/option.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | var Default = &Options{ 4 | spec: "@every 1h", 5 | selectTimeout: 10, //unit of second 6 | } 7 | 8 | type Option func(options *Options) 9 | 10 | type Options struct { 11 | spec string 12 | selectTimeout int 13 | } 14 | 15 | func WithSpec(spec string) Option { 16 | return func(options *Options) { 17 | if spec == "" { 18 | return 19 | } 20 | options.spec = spec 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dao/ctx.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wuqinqiang/helloword/dao/query" 7 | "github.com/wuqinqiang/helloword/db" 8 | ) 9 | 10 | const ( 11 | limit = 10000 12 | ) 13 | 14 | func use(ctx context.Context) *query.Query { 15 | db := db.Get().WithContext(ctx) 16 | return query.Use(db) 17 | } 18 | -------------------------------------------------------------------------------- /dao/dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | type Dao struct { 4 | Word Word 5 | Phrase Phrase 6 | WordPhrase WordPhrase 7 | } 8 | 9 | func Get() Dao { 10 | return Dao{ 11 | Word: NewWord(), 12 | Phrase: NewPhrase(), 13 | WordPhrase: NewWordPhrase(), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dao/model/phrase.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | const TableNamePhrase = "phrase" 8 | 9 | // Phrase mapped from table 10 | type Phrase struct { 11 | PhraseID string `gorm:"column:phrase_id;type:TEXT" json:"phrase_id"` 12 | Phrase string `gorm:"column:phrase;type:TEXT" json:"phrase"` 13 | CreateTime int64 `gorm:"column:create_time;type:BIGINT" json:"create_time"` 14 | UpdateTime int64 `gorm:"column:update_time;type:BIGINT" json:"update_time"` 15 | } 16 | 17 | // TableName Phrase's table name 18 | func (*Phrase) TableName() string { 19 | return TableNamePhrase 20 | } 21 | -------------------------------------------------------------------------------- /dao/model/phrase.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | func NewPhrase(phrase string) *Phrase { 10 | now := time.Now().Unix() 11 | return &Phrase{ 12 | PhraseID: "p" + uuid.NewString(), 13 | Phrase: phrase, 14 | CreateTime: now, 15 | UpdateTime: now, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dao/model/word.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | const TableNameWord = "word" 8 | 9 | // Word mapped from table 10 | type Word struct { 11 | WordID string `gorm:"column:word_id;primaryKey" json:"word_id"` 12 | Word string `gorm:"column:word;not null" json:"word"` 13 | Phonetic string `gorm:"column:phonetic;not null" json:"phonetic"` 14 | Definition string `gorm:"column:definition;not null" json:"definition"` 15 | Difficulty string `gorm:"column:difficulty;not null" json:"difficulty"` 16 | LastUsed int64 `gorm:"column:last_used;not null" json:"last_used"` 17 | NumRepetitions int32 `gorm:"column:num_repetitions;not null" json:"num_repetitions"` 18 | CreateTime int64 `gorm:"column:create_time;not null" json:"create_time"` 19 | UpdateTime int64 `gorm:"column:update_time;not null" json:"update_time"` 20 | } 21 | 22 | // TableName Word's table name 23 | func (*Word) TableName() string { 24 | return TableNameWord 25 | } 26 | -------------------------------------------------------------------------------- /dao/model/word.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type Words []*Word 11 | 12 | func NewWord(word string) *Word { 13 | now := time.Now().Unix() 14 | return &Word{ 15 | WordID: uuid.NewString(), 16 | Word: word, 17 | CreateTime: now, 18 | UpdateTime: now, 19 | } 20 | } 21 | 22 | func (word *Word) SetDefinition(definition string) { 23 | word.Definition = definition 24 | } 25 | 26 | func (word *Word) SetPhonetic(phonetic string) { 27 | word.Phonetic = phonetic 28 | } 29 | 30 | func (words Words) ListByLetter() map[string]Words { 31 | m := make(map[string]Words) 32 | for _, item := range words { 33 | if len(item.Word) == 0 { 34 | continue 35 | } 36 | startLetter := item.Word[0:1] 37 | m[startLetter] = append(m[startLetter], item) 38 | } 39 | return m 40 | } 41 | 42 | func (words Words) RandomPick() *Word { 43 | rand.Seed(time.Now().UnixNano()) 44 | return words[rand.Intn(len(words))] 45 | } 46 | 47 | func (words Words) List() (items []string) { 48 | for i := range words { 49 | items = append(items, words[i].Word) 50 | } 51 | return 52 | } 53 | func (words Words) WordIds() (items []string) { 54 | for i := range words { 55 | items = append(items, words[i].WordID) 56 | } 57 | return 58 | } 59 | 60 | func (words Words) GenerateWordPhrase(phraseId string) (list []*WordPhrase) { 61 | for _, word := range words { 62 | list = append(list, NewWordPhrase(word.WordID, phraseId)) 63 | } 64 | return list 65 | } 66 | -------------------------------------------------------------------------------- /dao/model/word_phrase.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | const TableNameWordPhrase = "word_phrase" 8 | 9 | // WordPhrase mapped from table 10 | type WordPhrase struct { 11 | WordPhraseID string `gorm:"column:word_phrase_id;type:TEXT" json:"word_phrase_id"` 12 | WordID string `gorm:"column:word_id;type:TEXT" json:"word_id"` 13 | PhraseID string `gorm:"column:phrase_id;type:TEXT" json:"phrase_id"` 14 | CreateTime int64 `gorm:"column:create_time;type:BIGINT" json:"create_time"` 15 | UpdateTime int64 `gorm:"column:update_time;type:BIGINT" json:"update_time"` 16 | } 17 | 18 | // TableName WordPhrase's table name 19 | func (*WordPhrase) TableName() string { 20 | return TableNameWordPhrase 21 | } 22 | -------------------------------------------------------------------------------- /dao/model/word_phrase.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | func NewWordPhrase(wordId, phraseId string) *WordPhrase { 10 | now := time.Now().Unix() 11 | return &WordPhrase{ 12 | WordPhraseID: "wp" + uuid.NewString(), 13 | WordID: wordId, 14 | PhraseID: phraseId, 15 | CreateTime: now, 16 | UpdateTime: now, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /dao/phrase.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wuqinqiang/helloword/dao/model" 7 | ) 8 | 9 | type Phrase interface { 10 | Create(ctx context.Context, phrase *model.Phrase) error 11 | } 12 | 13 | type PhraseImpl struct { 14 | } 15 | 16 | func NewPhrase() Phrase { 17 | return &PhraseImpl{} 18 | } 19 | 20 | func (impl PhraseImpl) Create(ctx context.Context, phrase *model.Phrase) error { 21 | w := use(ctx).Phrase 22 | return w.WithContext(ctx).Create(phrase) 23 | } 24 | -------------------------------------------------------------------------------- /dao/query/phrase.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package query 6 | 7 | import ( 8 | "context" 9 | 10 | "gorm.io/gorm" 11 | "gorm.io/gorm/clause" 12 | "gorm.io/gorm/schema" 13 | 14 | "gorm.io/gen" 15 | "gorm.io/gen/field" 16 | 17 | "gorm.io/plugin/dbresolver" 18 | 19 | "github.com/wuqinqiang/helloword/dao/model" 20 | ) 21 | 22 | func newPhrase(db *gorm.DB, opts ...gen.DOOption) phrase { 23 | _phrase := phrase{} 24 | 25 | _phrase.phraseDo.UseDB(db, opts...) 26 | _phrase.phraseDo.UseModel(&model.Phrase{}) 27 | 28 | tableName := _phrase.phraseDo.TableName() 29 | _phrase.ALL = field.NewAsterisk(tableName) 30 | _phrase.PhraseID = field.NewString(tableName, "phrase_id") 31 | _phrase.Phrase = field.NewString(tableName, "phrase") 32 | _phrase.CreateTime = field.NewInt64(tableName, "create_time") 33 | _phrase.UpdateTime = field.NewInt64(tableName, "update_time") 34 | 35 | _phrase.fillFieldMap() 36 | 37 | return _phrase 38 | } 39 | 40 | type phrase struct { 41 | phraseDo phraseDo 42 | 43 | ALL field.Asterisk 44 | PhraseID field.String 45 | Phrase field.String 46 | CreateTime field.Int64 47 | UpdateTime field.Int64 48 | 49 | fieldMap map[string]field.Expr 50 | } 51 | 52 | func (p phrase) Table(newTableName string) *phrase { 53 | p.phraseDo.UseTable(newTableName) 54 | return p.updateTableName(newTableName) 55 | } 56 | 57 | func (p phrase) As(alias string) *phrase { 58 | p.phraseDo.DO = *(p.phraseDo.As(alias).(*gen.DO)) 59 | return p.updateTableName(alias) 60 | } 61 | 62 | func (p *phrase) updateTableName(table string) *phrase { 63 | p.ALL = field.NewAsterisk(table) 64 | p.PhraseID = field.NewString(table, "phrase_id") 65 | p.Phrase = field.NewString(table, "phrase") 66 | p.CreateTime = field.NewInt64(table, "create_time") 67 | p.UpdateTime = field.NewInt64(table, "update_time") 68 | 69 | p.fillFieldMap() 70 | 71 | return p 72 | } 73 | 74 | func (p *phrase) WithContext(ctx context.Context) *phraseDo { return p.phraseDo.WithContext(ctx) } 75 | 76 | func (p phrase) TableName() string { return p.phraseDo.TableName() } 77 | 78 | func (p phrase) Alias() string { return p.phraseDo.Alias() } 79 | 80 | func (p *phrase) GetFieldByName(fieldName string) (field.OrderExpr, bool) { 81 | _f, ok := p.fieldMap[fieldName] 82 | if !ok || _f == nil { 83 | return nil, false 84 | } 85 | _oe, ok := _f.(field.OrderExpr) 86 | return _oe, ok 87 | } 88 | 89 | func (p *phrase) fillFieldMap() { 90 | p.fieldMap = make(map[string]field.Expr, 4) 91 | p.fieldMap["phrase_id"] = p.PhraseID 92 | p.fieldMap["phrase"] = p.Phrase 93 | p.fieldMap["create_time"] = p.CreateTime 94 | p.fieldMap["update_time"] = p.UpdateTime 95 | } 96 | 97 | func (p phrase) clone(db *gorm.DB) phrase { 98 | p.phraseDo.ReplaceConnPool(db.Statement.ConnPool) 99 | return p 100 | } 101 | 102 | func (p phrase) replaceDB(db *gorm.DB) phrase { 103 | p.phraseDo.ReplaceDB(db) 104 | return p 105 | } 106 | 107 | type phraseDo struct{ gen.DO } 108 | 109 | func (p phraseDo) Debug() *phraseDo { 110 | return p.withDO(p.DO.Debug()) 111 | } 112 | 113 | func (p phraseDo) WithContext(ctx context.Context) *phraseDo { 114 | return p.withDO(p.DO.WithContext(ctx)) 115 | } 116 | 117 | func (p phraseDo) ReadDB() *phraseDo { 118 | return p.Clauses(dbresolver.Read) 119 | } 120 | 121 | func (p phraseDo) WriteDB() *phraseDo { 122 | return p.Clauses(dbresolver.Write) 123 | } 124 | 125 | func (p phraseDo) Session(config *gorm.Session) *phraseDo { 126 | return p.withDO(p.DO.Session(config)) 127 | } 128 | 129 | func (p phraseDo) Clauses(conds ...clause.Expression) *phraseDo { 130 | return p.withDO(p.DO.Clauses(conds...)) 131 | } 132 | 133 | func (p phraseDo) Returning(value interface{}, columns ...string) *phraseDo { 134 | return p.withDO(p.DO.Returning(value, columns...)) 135 | } 136 | 137 | func (p phraseDo) Not(conds ...gen.Condition) *phraseDo { 138 | return p.withDO(p.DO.Not(conds...)) 139 | } 140 | 141 | func (p phraseDo) Or(conds ...gen.Condition) *phraseDo { 142 | return p.withDO(p.DO.Or(conds...)) 143 | } 144 | 145 | func (p phraseDo) Select(conds ...field.Expr) *phraseDo { 146 | return p.withDO(p.DO.Select(conds...)) 147 | } 148 | 149 | func (p phraseDo) Where(conds ...gen.Condition) *phraseDo { 150 | return p.withDO(p.DO.Where(conds...)) 151 | } 152 | 153 | func (p phraseDo) Exists(subquery interface{ UnderlyingDB() *gorm.DB }) *phraseDo { 154 | return p.Where(field.CompareSubQuery(field.ExistsOp, nil, subquery.UnderlyingDB())) 155 | } 156 | 157 | func (p phraseDo) Order(conds ...field.Expr) *phraseDo { 158 | return p.withDO(p.DO.Order(conds...)) 159 | } 160 | 161 | func (p phraseDo) Distinct(cols ...field.Expr) *phraseDo { 162 | return p.withDO(p.DO.Distinct(cols...)) 163 | } 164 | 165 | func (p phraseDo) Omit(cols ...field.Expr) *phraseDo { 166 | return p.withDO(p.DO.Omit(cols...)) 167 | } 168 | 169 | func (p phraseDo) Join(table schema.Tabler, on ...field.Expr) *phraseDo { 170 | return p.withDO(p.DO.Join(table, on...)) 171 | } 172 | 173 | func (p phraseDo) LeftJoin(table schema.Tabler, on ...field.Expr) *phraseDo { 174 | return p.withDO(p.DO.LeftJoin(table, on...)) 175 | } 176 | 177 | func (p phraseDo) RightJoin(table schema.Tabler, on ...field.Expr) *phraseDo { 178 | return p.withDO(p.DO.RightJoin(table, on...)) 179 | } 180 | 181 | func (p phraseDo) Group(cols ...field.Expr) *phraseDo { 182 | return p.withDO(p.DO.Group(cols...)) 183 | } 184 | 185 | func (p phraseDo) Having(conds ...gen.Condition) *phraseDo { 186 | return p.withDO(p.DO.Having(conds...)) 187 | } 188 | 189 | func (p phraseDo) Limit(limit int) *phraseDo { 190 | return p.withDO(p.DO.Limit(limit)) 191 | } 192 | 193 | func (p phraseDo) Offset(offset int) *phraseDo { 194 | return p.withDO(p.DO.Offset(offset)) 195 | } 196 | 197 | func (p phraseDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *phraseDo { 198 | return p.withDO(p.DO.Scopes(funcs...)) 199 | } 200 | 201 | func (p phraseDo) Unscoped() *phraseDo { 202 | return p.withDO(p.DO.Unscoped()) 203 | } 204 | 205 | func (p phraseDo) Create(values ...*model.Phrase) error { 206 | if len(values) == 0 { 207 | return nil 208 | } 209 | return p.DO.Create(values) 210 | } 211 | 212 | func (p phraseDo) CreateInBatches(values []*model.Phrase, batchSize int) error { 213 | return p.DO.CreateInBatches(values, batchSize) 214 | } 215 | 216 | // Save : !!! underlying implementation is different with GORM 217 | // The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) 218 | func (p phraseDo) Save(values ...*model.Phrase) error { 219 | if len(values) == 0 { 220 | return nil 221 | } 222 | return p.DO.Save(values) 223 | } 224 | 225 | func (p phraseDo) First() (*model.Phrase, error) { 226 | if result, err := p.DO.First(); err != nil { 227 | return nil, err 228 | } else { 229 | return result.(*model.Phrase), nil 230 | } 231 | } 232 | 233 | func (p phraseDo) Take() (*model.Phrase, error) { 234 | if result, err := p.DO.Take(); err != nil { 235 | return nil, err 236 | } else { 237 | return result.(*model.Phrase), nil 238 | } 239 | } 240 | 241 | func (p phraseDo) Last() (*model.Phrase, error) { 242 | if result, err := p.DO.Last(); err != nil { 243 | return nil, err 244 | } else { 245 | return result.(*model.Phrase), nil 246 | } 247 | } 248 | 249 | func (p phraseDo) Find() ([]*model.Phrase, error) { 250 | result, err := p.DO.Find() 251 | return result.([]*model.Phrase), err 252 | } 253 | 254 | func (p phraseDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Phrase, err error) { 255 | buf := make([]*model.Phrase, 0, batchSize) 256 | err = p.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { 257 | defer func() { results = append(results, buf...) }() 258 | return fc(tx, batch) 259 | }) 260 | return results, err 261 | } 262 | 263 | func (p phraseDo) FindInBatches(result *[]*model.Phrase, batchSize int, fc func(tx gen.Dao, batch int) error) error { 264 | return p.DO.FindInBatches(result, batchSize, fc) 265 | } 266 | 267 | func (p phraseDo) Attrs(attrs ...field.AssignExpr) *phraseDo { 268 | return p.withDO(p.DO.Attrs(attrs...)) 269 | } 270 | 271 | func (p phraseDo) Assign(attrs ...field.AssignExpr) *phraseDo { 272 | return p.withDO(p.DO.Assign(attrs...)) 273 | } 274 | 275 | func (p phraseDo) Joins(fields ...field.RelationField) *phraseDo { 276 | for _, _f := range fields { 277 | p = *p.withDO(p.DO.Joins(_f)) 278 | } 279 | return &p 280 | } 281 | 282 | func (p phraseDo) Preload(fields ...field.RelationField) *phraseDo { 283 | for _, _f := range fields { 284 | p = *p.withDO(p.DO.Preload(_f)) 285 | } 286 | return &p 287 | } 288 | 289 | func (p phraseDo) FirstOrInit() (*model.Phrase, error) { 290 | if result, err := p.DO.FirstOrInit(); err != nil { 291 | return nil, err 292 | } else { 293 | return result.(*model.Phrase), nil 294 | } 295 | } 296 | 297 | func (p phraseDo) FirstOrCreate() (*model.Phrase, error) { 298 | if result, err := p.DO.FirstOrCreate(); err != nil { 299 | return nil, err 300 | } else { 301 | return result.(*model.Phrase), nil 302 | } 303 | } 304 | 305 | func (p phraseDo) FindByPage(offset int, limit int) (result []*model.Phrase, count int64, err error) { 306 | result, err = p.Offset(offset).Limit(limit).Find() 307 | if err != nil { 308 | return 309 | } 310 | 311 | if size := len(result); 0 < limit && 0 < size && size < limit { 312 | count = int64(size + offset) 313 | return 314 | } 315 | 316 | count, err = p.Offset(-1).Limit(-1).Count() 317 | return 318 | } 319 | 320 | func (p phraseDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { 321 | count, err = p.Count() 322 | if err != nil { 323 | return 324 | } 325 | 326 | err = p.Offset(offset).Limit(limit).Scan(result) 327 | return 328 | } 329 | 330 | func (p phraseDo) Scan(result interface{}) (err error) { 331 | return p.DO.Scan(result) 332 | } 333 | 334 | func (p phraseDo) Delete(models ...*model.Phrase) (result gen.ResultInfo, err error) { 335 | return p.DO.Delete(models) 336 | } 337 | 338 | func (p *phraseDo) withDO(do gen.Dao) *phraseDo { 339 | p.DO = *do.(*gen.DO) 340 | return p 341 | } 342 | -------------------------------------------------------------------------------- /dao/query/query.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package query 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | 11 | "gorm.io/gorm" 12 | 13 | "gorm.io/gen" 14 | 15 | "gorm.io/plugin/dbresolver" 16 | ) 17 | 18 | func Use(db *gorm.DB, opts ...gen.DOOption) *Query { 19 | return &Query{ 20 | db: db, 21 | Phrase: newPhrase(db, opts...), 22 | Word: newWord(db, opts...), 23 | WordPhrase: newWordPhrase(db, opts...), 24 | } 25 | } 26 | 27 | type Query struct { 28 | db *gorm.DB 29 | 30 | Phrase phrase 31 | Word word 32 | WordPhrase wordPhrase 33 | } 34 | 35 | func (q *Query) Available() bool { return q.db != nil } 36 | 37 | func (q *Query) clone(db *gorm.DB) *Query { 38 | return &Query{ 39 | db: db, 40 | Phrase: q.Phrase.clone(db), 41 | Word: q.Word.clone(db), 42 | WordPhrase: q.WordPhrase.clone(db), 43 | } 44 | } 45 | 46 | func (q *Query) ReadDB() *Query { 47 | return q.ReplaceDB(q.db.Clauses(dbresolver.Read)) 48 | } 49 | 50 | func (q *Query) WriteDB() *Query { 51 | return q.ReplaceDB(q.db.Clauses(dbresolver.Write)) 52 | } 53 | 54 | func (q *Query) ReplaceDB(db *gorm.DB) *Query { 55 | return &Query{ 56 | db: db, 57 | Phrase: q.Phrase.replaceDB(db), 58 | Word: q.Word.replaceDB(db), 59 | WordPhrase: q.WordPhrase.replaceDB(db), 60 | } 61 | } 62 | 63 | type queryCtx struct { 64 | Phrase *phraseDo 65 | Word *wordDo 66 | WordPhrase *wordPhraseDo 67 | } 68 | 69 | func (q *Query) WithContext(ctx context.Context) *queryCtx { 70 | return &queryCtx{ 71 | Phrase: q.Phrase.WithContext(ctx), 72 | Word: q.Word.WithContext(ctx), 73 | WordPhrase: q.WordPhrase.WithContext(ctx), 74 | } 75 | } 76 | 77 | func (q *Query) Transaction(fc func(tx *Query) error, opts ...*sql.TxOptions) error { 78 | return q.db.Transaction(func(tx *gorm.DB) error { return fc(q.clone(tx)) }, opts...) 79 | } 80 | 81 | func (q *Query) Begin(opts ...*sql.TxOptions) *QueryTx { 82 | return &QueryTx{q.clone(q.db.Begin(opts...))} 83 | } 84 | 85 | type QueryTx struct{ *Query } 86 | 87 | func (q *QueryTx) Commit() error { 88 | return q.db.Commit().Error 89 | } 90 | 91 | func (q *QueryTx) Rollback() error { 92 | return q.db.Rollback().Error 93 | } 94 | 95 | func (q *QueryTx) SavePoint(name string) error { 96 | return q.db.SavePoint(name).Error 97 | } 98 | 99 | func (q *QueryTx) RollbackTo(name string) error { 100 | return q.db.RollbackTo(name).Error 101 | } 102 | -------------------------------------------------------------------------------- /dao/query/word.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package query 6 | 7 | import ( 8 | "context" 9 | 10 | "gorm.io/gorm" 11 | "gorm.io/gorm/clause" 12 | "gorm.io/gorm/schema" 13 | 14 | "gorm.io/gen" 15 | "gorm.io/gen/field" 16 | 17 | "gorm.io/plugin/dbresolver" 18 | 19 | "github.com/wuqinqiang/helloword/dao/model" 20 | ) 21 | 22 | func newWord(db *gorm.DB, opts ...gen.DOOption) word { 23 | _word := word{} 24 | 25 | _word.wordDo.UseDB(db, opts...) 26 | _word.wordDo.UseModel(&model.Word{}) 27 | 28 | tableName := _word.wordDo.TableName() 29 | _word.ALL = field.NewAsterisk(tableName) 30 | _word.WordID = field.NewString(tableName, "word_id") 31 | _word.Word = field.NewString(tableName, "word") 32 | _word.Phonetic = field.NewString(tableName, "phonetic") 33 | _word.Definition = field.NewString(tableName, "definition") 34 | _word.Difficulty = field.NewString(tableName, "difficulty") 35 | _word.LastUsed = field.NewInt64(tableName, "last_used") 36 | _word.NumRepetitions = field.NewInt32(tableName, "num_repetitions") 37 | _word.CreateTime = field.NewInt64(tableName, "create_time") 38 | _word.UpdateTime = field.NewInt64(tableName, "update_time") 39 | 40 | _word.fillFieldMap() 41 | 42 | return _word 43 | } 44 | 45 | type word struct { 46 | wordDo wordDo 47 | 48 | ALL field.Asterisk 49 | WordID field.String 50 | Word field.String 51 | Phonetic field.String 52 | Definition field.String 53 | Difficulty field.String 54 | LastUsed field.Int64 55 | NumRepetitions field.Int32 56 | CreateTime field.Int64 57 | UpdateTime field.Int64 58 | 59 | fieldMap map[string]field.Expr 60 | } 61 | 62 | func (w word) Table(newTableName string) *word { 63 | w.wordDo.UseTable(newTableName) 64 | return w.updateTableName(newTableName) 65 | } 66 | 67 | func (w word) As(alias string) *word { 68 | w.wordDo.DO = *(w.wordDo.As(alias).(*gen.DO)) 69 | return w.updateTableName(alias) 70 | } 71 | 72 | func (w *word) updateTableName(table string) *word { 73 | w.ALL = field.NewAsterisk(table) 74 | w.WordID = field.NewString(table, "word_id") 75 | w.Word = field.NewString(table, "word") 76 | w.Phonetic = field.NewString(table, "phonetic") 77 | w.Definition = field.NewString(table, "definition") 78 | w.Difficulty = field.NewString(table, "difficulty") 79 | w.LastUsed = field.NewInt64(table, "last_used") 80 | w.NumRepetitions = field.NewInt32(table, "num_repetitions") 81 | w.CreateTime = field.NewInt64(table, "create_time") 82 | w.UpdateTime = field.NewInt64(table, "update_time") 83 | 84 | w.fillFieldMap() 85 | 86 | return w 87 | } 88 | 89 | func (w *word) WithContext(ctx context.Context) *wordDo { return w.wordDo.WithContext(ctx) } 90 | 91 | func (w word) TableName() string { return w.wordDo.TableName() } 92 | 93 | func (w word) Alias() string { return w.wordDo.Alias() } 94 | 95 | func (w *word) GetFieldByName(fieldName string) (field.OrderExpr, bool) { 96 | _f, ok := w.fieldMap[fieldName] 97 | if !ok || _f == nil { 98 | return nil, false 99 | } 100 | _oe, ok := _f.(field.OrderExpr) 101 | return _oe, ok 102 | } 103 | 104 | func (w *word) fillFieldMap() { 105 | w.fieldMap = make(map[string]field.Expr, 9) 106 | w.fieldMap["word_id"] = w.WordID 107 | w.fieldMap["word"] = w.Word 108 | w.fieldMap["phonetic"] = w.Phonetic 109 | w.fieldMap["definition"] = w.Definition 110 | w.fieldMap["difficulty"] = w.Difficulty 111 | w.fieldMap["last_used"] = w.LastUsed 112 | w.fieldMap["num_repetitions"] = w.NumRepetitions 113 | w.fieldMap["create_time"] = w.CreateTime 114 | w.fieldMap["update_time"] = w.UpdateTime 115 | } 116 | 117 | func (w word) clone(db *gorm.DB) word { 118 | w.wordDo.ReplaceConnPool(db.Statement.ConnPool) 119 | return w 120 | } 121 | 122 | func (w word) replaceDB(db *gorm.DB) word { 123 | w.wordDo.ReplaceDB(db) 124 | return w 125 | } 126 | 127 | type wordDo struct{ gen.DO } 128 | 129 | func (w wordDo) Debug() *wordDo { 130 | return w.withDO(w.DO.Debug()) 131 | } 132 | 133 | func (w wordDo) WithContext(ctx context.Context) *wordDo { 134 | return w.withDO(w.DO.WithContext(ctx)) 135 | } 136 | 137 | func (w wordDo) ReadDB() *wordDo { 138 | return w.Clauses(dbresolver.Read) 139 | } 140 | 141 | func (w wordDo) WriteDB() *wordDo { 142 | return w.Clauses(dbresolver.Write) 143 | } 144 | 145 | func (w wordDo) Session(config *gorm.Session) *wordDo { 146 | return w.withDO(w.DO.Session(config)) 147 | } 148 | 149 | func (w wordDo) Clauses(conds ...clause.Expression) *wordDo { 150 | return w.withDO(w.DO.Clauses(conds...)) 151 | } 152 | 153 | func (w wordDo) Returning(value interface{}, columns ...string) *wordDo { 154 | return w.withDO(w.DO.Returning(value, columns...)) 155 | } 156 | 157 | func (w wordDo) Not(conds ...gen.Condition) *wordDo { 158 | return w.withDO(w.DO.Not(conds...)) 159 | } 160 | 161 | func (w wordDo) Or(conds ...gen.Condition) *wordDo { 162 | return w.withDO(w.DO.Or(conds...)) 163 | } 164 | 165 | func (w wordDo) Select(conds ...field.Expr) *wordDo { 166 | return w.withDO(w.DO.Select(conds...)) 167 | } 168 | 169 | func (w wordDo) Where(conds ...gen.Condition) *wordDo { 170 | return w.withDO(w.DO.Where(conds...)) 171 | } 172 | 173 | func (w wordDo) Exists(subquery interface{ UnderlyingDB() *gorm.DB }) *wordDo { 174 | return w.Where(field.CompareSubQuery(field.ExistsOp, nil, subquery.UnderlyingDB())) 175 | } 176 | 177 | func (w wordDo) Order(conds ...field.Expr) *wordDo { 178 | return w.withDO(w.DO.Order(conds...)) 179 | } 180 | 181 | func (w wordDo) Distinct(cols ...field.Expr) *wordDo { 182 | return w.withDO(w.DO.Distinct(cols...)) 183 | } 184 | 185 | func (w wordDo) Omit(cols ...field.Expr) *wordDo { 186 | return w.withDO(w.DO.Omit(cols...)) 187 | } 188 | 189 | func (w wordDo) Join(table schema.Tabler, on ...field.Expr) *wordDo { 190 | return w.withDO(w.DO.Join(table, on...)) 191 | } 192 | 193 | func (w wordDo) LeftJoin(table schema.Tabler, on ...field.Expr) *wordDo { 194 | return w.withDO(w.DO.LeftJoin(table, on...)) 195 | } 196 | 197 | func (w wordDo) RightJoin(table schema.Tabler, on ...field.Expr) *wordDo { 198 | return w.withDO(w.DO.RightJoin(table, on...)) 199 | } 200 | 201 | func (w wordDo) Group(cols ...field.Expr) *wordDo { 202 | return w.withDO(w.DO.Group(cols...)) 203 | } 204 | 205 | func (w wordDo) Having(conds ...gen.Condition) *wordDo { 206 | return w.withDO(w.DO.Having(conds...)) 207 | } 208 | 209 | func (w wordDo) Limit(limit int) *wordDo { 210 | return w.withDO(w.DO.Limit(limit)) 211 | } 212 | 213 | func (w wordDo) Offset(offset int) *wordDo { 214 | return w.withDO(w.DO.Offset(offset)) 215 | } 216 | 217 | func (w wordDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *wordDo { 218 | return w.withDO(w.DO.Scopes(funcs...)) 219 | } 220 | 221 | func (w wordDo) Unscoped() *wordDo { 222 | return w.withDO(w.DO.Unscoped()) 223 | } 224 | 225 | func (w wordDo) Create(values ...*model.Word) error { 226 | if len(values) == 0 { 227 | return nil 228 | } 229 | return w.DO.Create(values) 230 | } 231 | 232 | func (w wordDo) CreateInBatches(values []*model.Word, batchSize int) error { 233 | return w.DO.CreateInBatches(values, batchSize) 234 | } 235 | 236 | // Save : !!! underlying implementation is different with GORM 237 | // The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) 238 | func (w wordDo) Save(values ...*model.Word) error { 239 | if len(values) == 0 { 240 | return nil 241 | } 242 | return w.DO.Save(values) 243 | } 244 | 245 | func (w wordDo) First() (*model.Word, error) { 246 | if result, err := w.DO.First(); err != nil { 247 | return nil, err 248 | } else { 249 | return result.(*model.Word), nil 250 | } 251 | } 252 | 253 | func (w wordDo) Take() (*model.Word, error) { 254 | if result, err := w.DO.Take(); err != nil { 255 | return nil, err 256 | } else { 257 | return result.(*model.Word), nil 258 | } 259 | } 260 | 261 | func (w wordDo) Last() (*model.Word, error) { 262 | if result, err := w.DO.Last(); err != nil { 263 | return nil, err 264 | } else { 265 | return result.(*model.Word), nil 266 | } 267 | } 268 | 269 | func (w wordDo) Find() ([]*model.Word, error) { 270 | result, err := w.DO.Find() 271 | return result.([]*model.Word), err 272 | } 273 | 274 | func (w wordDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Word, err error) { 275 | buf := make([]*model.Word, 0, batchSize) 276 | err = w.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { 277 | defer func() { results = append(results, buf...) }() 278 | return fc(tx, batch) 279 | }) 280 | return results, err 281 | } 282 | 283 | func (w wordDo) FindInBatches(result *[]*model.Word, batchSize int, fc func(tx gen.Dao, batch int) error) error { 284 | return w.DO.FindInBatches(result, batchSize, fc) 285 | } 286 | 287 | func (w wordDo) Attrs(attrs ...field.AssignExpr) *wordDo { 288 | return w.withDO(w.DO.Attrs(attrs...)) 289 | } 290 | 291 | func (w wordDo) Assign(attrs ...field.AssignExpr) *wordDo { 292 | return w.withDO(w.DO.Assign(attrs...)) 293 | } 294 | 295 | func (w wordDo) Joins(fields ...field.RelationField) *wordDo { 296 | for _, _f := range fields { 297 | w = *w.withDO(w.DO.Joins(_f)) 298 | } 299 | return &w 300 | } 301 | 302 | func (w wordDo) Preload(fields ...field.RelationField) *wordDo { 303 | for _, _f := range fields { 304 | w = *w.withDO(w.DO.Preload(_f)) 305 | } 306 | return &w 307 | } 308 | 309 | func (w wordDo) FirstOrInit() (*model.Word, error) { 310 | if result, err := w.DO.FirstOrInit(); err != nil { 311 | return nil, err 312 | } else { 313 | return result.(*model.Word), nil 314 | } 315 | } 316 | 317 | func (w wordDo) FirstOrCreate() (*model.Word, error) { 318 | if result, err := w.DO.FirstOrCreate(); err != nil { 319 | return nil, err 320 | } else { 321 | return result.(*model.Word), nil 322 | } 323 | } 324 | 325 | func (w wordDo) FindByPage(offset int, limit int) (result []*model.Word, count int64, err error) { 326 | result, err = w.Offset(offset).Limit(limit).Find() 327 | if err != nil { 328 | return 329 | } 330 | 331 | if size := len(result); 0 < limit && 0 < size && size < limit { 332 | count = int64(size + offset) 333 | return 334 | } 335 | 336 | count, err = w.Offset(-1).Limit(-1).Count() 337 | return 338 | } 339 | 340 | func (w wordDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { 341 | count, err = w.Count() 342 | if err != nil { 343 | return 344 | } 345 | 346 | err = w.Offset(offset).Limit(limit).Scan(result) 347 | return 348 | } 349 | 350 | func (w wordDo) Scan(result interface{}) (err error) { 351 | return w.DO.Scan(result) 352 | } 353 | 354 | func (w wordDo) Delete(models ...*model.Word) (result gen.ResultInfo, err error) { 355 | return w.DO.Delete(models) 356 | } 357 | 358 | func (w *wordDo) withDO(do gen.Dao) *wordDo { 359 | w.DO = *do.(*gen.DO) 360 | return w 361 | } 362 | -------------------------------------------------------------------------------- /dao/query/word_phrase.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package query 6 | 7 | import ( 8 | "context" 9 | 10 | "gorm.io/gorm" 11 | "gorm.io/gorm/clause" 12 | "gorm.io/gorm/schema" 13 | 14 | "gorm.io/gen" 15 | "gorm.io/gen/field" 16 | 17 | "gorm.io/plugin/dbresolver" 18 | 19 | "github.com/wuqinqiang/helloword/dao/model" 20 | ) 21 | 22 | func newWordPhrase(db *gorm.DB, opts ...gen.DOOption) wordPhrase { 23 | _wordPhrase := wordPhrase{} 24 | 25 | _wordPhrase.wordPhraseDo.UseDB(db, opts...) 26 | _wordPhrase.wordPhraseDo.UseModel(&model.WordPhrase{}) 27 | 28 | tableName := _wordPhrase.wordPhraseDo.TableName() 29 | _wordPhrase.ALL = field.NewAsterisk(tableName) 30 | _wordPhrase.WordPhraseID = field.NewString(tableName, "word_phrase_id") 31 | _wordPhrase.WordID = field.NewString(tableName, "word_id") 32 | _wordPhrase.PhraseID = field.NewString(tableName, "phrase_id") 33 | _wordPhrase.CreateTime = field.NewInt64(tableName, "create_time") 34 | _wordPhrase.UpdateTime = field.NewInt64(tableName, "update_time") 35 | 36 | _wordPhrase.fillFieldMap() 37 | 38 | return _wordPhrase 39 | } 40 | 41 | type wordPhrase struct { 42 | wordPhraseDo wordPhraseDo 43 | 44 | ALL field.Asterisk 45 | WordPhraseID field.String 46 | WordID field.String 47 | PhraseID field.String 48 | CreateTime field.Int64 49 | UpdateTime field.Int64 50 | 51 | fieldMap map[string]field.Expr 52 | } 53 | 54 | func (w wordPhrase) Table(newTableName string) *wordPhrase { 55 | w.wordPhraseDo.UseTable(newTableName) 56 | return w.updateTableName(newTableName) 57 | } 58 | 59 | func (w wordPhrase) As(alias string) *wordPhrase { 60 | w.wordPhraseDo.DO = *(w.wordPhraseDo.As(alias).(*gen.DO)) 61 | return w.updateTableName(alias) 62 | } 63 | 64 | func (w *wordPhrase) updateTableName(table string) *wordPhrase { 65 | w.ALL = field.NewAsterisk(table) 66 | w.WordPhraseID = field.NewString(table, "word_phrase_id") 67 | w.WordID = field.NewString(table, "word_id") 68 | w.PhraseID = field.NewString(table, "phrase_id") 69 | w.CreateTime = field.NewInt64(table, "create_time") 70 | w.UpdateTime = field.NewInt64(table, "update_time") 71 | 72 | w.fillFieldMap() 73 | 74 | return w 75 | } 76 | 77 | func (w *wordPhrase) WithContext(ctx context.Context) *wordPhraseDo { 78 | return w.wordPhraseDo.WithContext(ctx) 79 | } 80 | 81 | func (w wordPhrase) TableName() string { return w.wordPhraseDo.TableName() } 82 | 83 | func (w wordPhrase) Alias() string { return w.wordPhraseDo.Alias() } 84 | 85 | func (w *wordPhrase) GetFieldByName(fieldName string) (field.OrderExpr, bool) { 86 | _f, ok := w.fieldMap[fieldName] 87 | if !ok || _f == nil { 88 | return nil, false 89 | } 90 | _oe, ok := _f.(field.OrderExpr) 91 | return _oe, ok 92 | } 93 | 94 | func (w *wordPhrase) fillFieldMap() { 95 | w.fieldMap = make(map[string]field.Expr, 5) 96 | w.fieldMap["word_phrase_id"] = w.WordPhraseID 97 | w.fieldMap["word_id"] = w.WordID 98 | w.fieldMap["phrase_id"] = w.PhraseID 99 | w.fieldMap["create_time"] = w.CreateTime 100 | w.fieldMap["update_time"] = w.UpdateTime 101 | } 102 | 103 | func (w wordPhrase) clone(db *gorm.DB) wordPhrase { 104 | w.wordPhraseDo.ReplaceConnPool(db.Statement.ConnPool) 105 | return w 106 | } 107 | 108 | func (w wordPhrase) replaceDB(db *gorm.DB) wordPhrase { 109 | w.wordPhraseDo.ReplaceDB(db) 110 | return w 111 | } 112 | 113 | type wordPhraseDo struct{ gen.DO } 114 | 115 | func (w wordPhraseDo) Debug() *wordPhraseDo { 116 | return w.withDO(w.DO.Debug()) 117 | } 118 | 119 | func (w wordPhraseDo) WithContext(ctx context.Context) *wordPhraseDo { 120 | return w.withDO(w.DO.WithContext(ctx)) 121 | } 122 | 123 | func (w wordPhraseDo) ReadDB() *wordPhraseDo { 124 | return w.Clauses(dbresolver.Read) 125 | } 126 | 127 | func (w wordPhraseDo) WriteDB() *wordPhraseDo { 128 | return w.Clauses(dbresolver.Write) 129 | } 130 | 131 | func (w wordPhraseDo) Session(config *gorm.Session) *wordPhraseDo { 132 | return w.withDO(w.DO.Session(config)) 133 | } 134 | 135 | func (w wordPhraseDo) Clauses(conds ...clause.Expression) *wordPhraseDo { 136 | return w.withDO(w.DO.Clauses(conds...)) 137 | } 138 | 139 | func (w wordPhraseDo) Returning(value interface{}, columns ...string) *wordPhraseDo { 140 | return w.withDO(w.DO.Returning(value, columns...)) 141 | } 142 | 143 | func (w wordPhraseDo) Not(conds ...gen.Condition) *wordPhraseDo { 144 | return w.withDO(w.DO.Not(conds...)) 145 | } 146 | 147 | func (w wordPhraseDo) Or(conds ...gen.Condition) *wordPhraseDo { 148 | return w.withDO(w.DO.Or(conds...)) 149 | } 150 | 151 | func (w wordPhraseDo) Select(conds ...field.Expr) *wordPhraseDo { 152 | return w.withDO(w.DO.Select(conds...)) 153 | } 154 | 155 | func (w wordPhraseDo) Where(conds ...gen.Condition) *wordPhraseDo { 156 | return w.withDO(w.DO.Where(conds...)) 157 | } 158 | 159 | func (w wordPhraseDo) Exists(subquery interface{ UnderlyingDB() *gorm.DB }) *wordPhraseDo { 160 | return w.Where(field.CompareSubQuery(field.ExistsOp, nil, subquery.UnderlyingDB())) 161 | } 162 | 163 | func (w wordPhraseDo) Order(conds ...field.Expr) *wordPhraseDo { 164 | return w.withDO(w.DO.Order(conds...)) 165 | } 166 | 167 | func (w wordPhraseDo) Distinct(cols ...field.Expr) *wordPhraseDo { 168 | return w.withDO(w.DO.Distinct(cols...)) 169 | } 170 | 171 | func (w wordPhraseDo) Omit(cols ...field.Expr) *wordPhraseDo { 172 | return w.withDO(w.DO.Omit(cols...)) 173 | } 174 | 175 | func (w wordPhraseDo) Join(table schema.Tabler, on ...field.Expr) *wordPhraseDo { 176 | return w.withDO(w.DO.Join(table, on...)) 177 | } 178 | 179 | func (w wordPhraseDo) LeftJoin(table schema.Tabler, on ...field.Expr) *wordPhraseDo { 180 | return w.withDO(w.DO.LeftJoin(table, on...)) 181 | } 182 | 183 | func (w wordPhraseDo) RightJoin(table schema.Tabler, on ...field.Expr) *wordPhraseDo { 184 | return w.withDO(w.DO.RightJoin(table, on...)) 185 | } 186 | 187 | func (w wordPhraseDo) Group(cols ...field.Expr) *wordPhraseDo { 188 | return w.withDO(w.DO.Group(cols...)) 189 | } 190 | 191 | func (w wordPhraseDo) Having(conds ...gen.Condition) *wordPhraseDo { 192 | return w.withDO(w.DO.Having(conds...)) 193 | } 194 | 195 | func (w wordPhraseDo) Limit(limit int) *wordPhraseDo { 196 | return w.withDO(w.DO.Limit(limit)) 197 | } 198 | 199 | func (w wordPhraseDo) Offset(offset int) *wordPhraseDo { 200 | return w.withDO(w.DO.Offset(offset)) 201 | } 202 | 203 | func (w wordPhraseDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *wordPhraseDo { 204 | return w.withDO(w.DO.Scopes(funcs...)) 205 | } 206 | 207 | func (w wordPhraseDo) Unscoped() *wordPhraseDo { 208 | return w.withDO(w.DO.Unscoped()) 209 | } 210 | 211 | func (w wordPhraseDo) Create(values ...*model.WordPhrase) error { 212 | if len(values) == 0 { 213 | return nil 214 | } 215 | return w.DO.Create(values) 216 | } 217 | 218 | func (w wordPhraseDo) CreateInBatches(values []*model.WordPhrase, batchSize int) error { 219 | return w.DO.CreateInBatches(values, batchSize) 220 | } 221 | 222 | // Save : !!! underlying implementation is different with GORM 223 | // The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) 224 | func (w wordPhraseDo) Save(values ...*model.WordPhrase) error { 225 | if len(values) == 0 { 226 | return nil 227 | } 228 | return w.DO.Save(values) 229 | } 230 | 231 | func (w wordPhraseDo) First() (*model.WordPhrase, error) { 232 | if result, err := w.DO.First(); err != nil { 233 | return nil, err 234 | } else { 235 | return result.(*model.WordPhrase), nil 236 | } 237 | } 238 | 239 | func (w wordPhraseDo) Take() (*model.WordPhrase, error) { 240 | if result, err := w.DO.Take(); err != nil { 241 | return nil, err 242 | } else { 243 | return result.(*model.WordPhrase), nil 244 | } 245 | } 246 | 247 | func (w wordPhraseDo) Last() (*model.WordPhrase, error) { 248 | if result, err := w.DO.Last(); err != nil { 249 | return nil, err 250 | } else { 251 | return result.(*model.WordPhrase), nil 252 | } 253 | } 254 | 255 | func (w wordPhraseDo) Find() ([]*model.WordPhrase, error) { 256 | result, err := w.DO.Find() 257 | return result.([]*model.WordPhrase), err 258 | } 259 | 260 | func (w wordPhraseDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.WordPhrase, err error) { 261 | buf := make([]*model.WordPhrase, 0, batchSize) 262 | err = w.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { 263 | defer func() { results = append(results, buf...) }() 264 | return fc(tx, batch) 265 | }) 266 | return results, err 267 | } 268 | 269 | func (w wordPhraseDo) FindInBatches(result *[]*model.WordPhrase, batchSize int, fc func(tx gen.Dao, batch int) error) error { 270 | return w.DO.FindInBatches(result, batchSize, fc) 271 | } 272 | 273 | func (w wordPhraseDo) Attrs(attrs ...field.AssignExpr) *wordPhraseDo { 274 | return w.withDO(w.DO.Attrs(attrs...)) 275 | } 276 | 277 | func (w wordPhraseDo) Assign(attrs ...field.AssignExpr) *wordPhraseDo { 278 | return w.withDO(w.DO.Assign(attrs...)) 279 | } 280 | 281 | func (w wordPhraseDo) Joins(fields ...field.RelationField) *wordPhraseDo { 282 | for _, _f := range fields { 283 | w = *w.withDO(w.DO.Joins(_f)) 284 | } 285 | return &w 286 | } 287 | 288 | func (w wordPhraseDo) Preload(fields ...field.RelationField) *wordPhraseDo { 289 | for _, _f := range fields { 290 | w = *w.withDO(w.DO.Preload(_f)) 291 | } 292 | return &w 293 | } 294 | 295 | func (w wordPhraseDo) FirstOrInit() (*model.WordPhrase, error) { 296 | if result, err := w.DO.FirstOrInit(); err != nil { 297 | return nil, err 298 | } else { 299 | return result.(*model.WordPhrase), nil 300 | } 301 | } 302 | 303 | func (w wordPhraseDo) FirstOrCreate() (*model.WordPhrase, error) { 304 | if result, err := w.DO.FirstOrCreate(); err != nil { 305 | return nil, err 306 | } else { 307 | return result.(*model.WordPhrase), nil 308 | } 309 | } 310 | 311 | func (w wordPhraseDo) FindByPage(offset int, limit int) (result []*model.WordPhrase, count int64, err error) { 312 | result, err = w.Offset(offset).Limit(limit).Find() 313 | if err != nil { 314 | return 315 | } 316 | 317 | if size := len(result); 0 < limit && 0 < size && size < limit { 318 | count = int64(size + offset) 319 | return 320 | } 321 | 322 | count, err = w.Offset(-1).Limit(-1).Count() 323 | return 324 | } 325 | 326 | func (w wordPhraseDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { 327 | count, err = w.Count() 328 | if err != nil { 329 | return 330 | } 331 | 332 | err = w.Offset(offset).Limit(limit).Scan(result) 333 | return 334 | } 335 | 336 | func (w wordPhraseDo) Scan(result interface{}) (err error) { 337 | return w.DO.Scan(result) 338 | } 339 | 340 | func (w wordPhraseDo) Delete(models ...*model.WordPhrase) (result gen.ResultInfo, err error) { 341 | return w.DO.Delete(models) 342 | } 343 | 344 | func (w *wordPhraseDo) withDO(do gen.Dao) *wordPhraseDo { 345 | w.DO = *do.(*gen.DO) 346 | return w 347 | } 348 | -------------------------------------------------------------------------------- /dao/word.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "gorm.io/gorm/clause" 8 | 9 | "github.com/wuqinqiang/helloword/dao/model" 10 | ) 11 | 12 | type Word interface { 13 | BatchInsert(ctx context.Context, words model.Words) error 14 | GetList(ctx context.Context) (words model.Words, err error) 15 | IncrNumRepetitions(ctx context.Context, wordIds []string) error 16 | } 17 | 18 | type WordImpl struct { 19 | } 20 | 21 | func NewWord() Word { 22 | return &WordImpl{} 23 | } 24 | 25 | func (impl WordImpl) BatchInsert(ctx context.Context, words model.Words) error { 26 | w := use(ctx).Word 27 | return w.WithContext(ctx).Clauses(clause.OnConflict{ 28 | Columns: []clause.Column{{Name: "word"}}, 29 | DoUpdates: clause.AssignmentColumns([]string{"phonetic", "definition", "difficulty"}), 30 | }).CreateInBatches(words, 50) 31 | } 32 | 33 | func (impl WordImpl) IncrNumRepetitions(ctx context.Context, wordIds []string) error { 34 | w := use(ctx).Word 35 | now := time.Now().Unix() 36 | _, err := w.WithContext(ctx).Where(w.WordID.In(wordIds...)). 37 | UpdateSimple(w.NumRepetitions.Add(1), w.LastUsed.Value(now)) 38 | return err 39 | } 40 | 41 | func (impl WordImpl) GetList(ctx context.Context) (words model.Words, err error) { 42 | w := use(ctx).Word 43 | words, err = w.WithContext(ctx).Limit(limit).Find() 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /dao/word_phrase.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wuqinqiang/helloword/dao/model" 7 | ) 8 | 9 | type WordPhrase interface { 10 | BatchInsert(ctx context.Context, list []*model.WordPhrase) error 11 | } 12 | 13 | type WordPhraseImpl struct { 14 | } 15 | 16 | func NewWordPhrase() WordPhrase { 17 | return &WordPhraseImpl{} 18 | } 19 | 20 | func (impl WordPhraseImpl) BatchInsert(ctx context.Context, list []*model.WordPhrase) error { 21 | w := use(ctx).WordPhrase 22 | return w.WithContext(ctx).CreateInBatches(list, 50) 23 | } 24 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | var ( 10 | db *gorm.DB 11 | ) 12 | 13 | type Provider interface { 14 | Init() error 15 | GetDb() (*gorm.DB, error) 16 | } 17 | 18 | func Init(provider Provider) error { 19 | err := provider.Init() 20 | if err != nil { 21 | return err 22 | } 23 | db, err = provider.GetDb() 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | func Get() *gorm.DB { 31 | if db == nil { 32 | panic("db must not nil,please call the sqlite first") 33 | } 34 | return db 35 | } 36 | -------------------------------------------------------------------------------- /db/sqlite/helloword.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS word ( 2 | word_id TEXT NOT NULL, 3 | word TEXT NOT NULL UNIQUE , 4 | phonetic TEXT, 5 | definition TEXT, 6 | difficulty TEXT, 7 | last_used BIGINT, 8 | num_repetitions INT, 9 | create_time BIGINT NOT NULL, 10 | update_time BIGINT NOT NULL, 11 | PRIMARY KEY(word_id) 12 | ) WITHOUT ROWID; 13 | 14 | CREATE TABLE IF NOT EXISTS phrase( 15 | phrase_id TEXT NOT NULL, 16 | phrase TEXT NOT NULL, 17 | create_time BIGINT NOT NULL, 18 | update_time BIGINT NOT NULL, 19 | PRIMARY KEY(phrase_id) 20 | ) WITHOUT ROWID; 21 | 22 | 23 | 24 | 25 | CREATE TABLE IF NOT EXISTS word_phrase( 26 | word_phrase_id TEXT NOT NULL, 27 | word_id TEXT NOT NULL, 28 | phrase_id TEXT NOT NULL, 29 | create_time BIGINT NOT NULL, 30 | update_time BIGINT NOT NULL, 31 | PRIMARY KEY(word_phrase_id) 32 | ) WITHOUT ROWID; 33 | -------------------------------------------------------------------------------- /db/sqlite/option.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "gorm.io/gorm/logger" 9 | ) 10 | 11 | var DefaultSettings = &Settings{ 12 | maxLifetime: 7200, 13 | maxIdleConns: 5, 14 | maxOpenConns: 15, 15 | dbFileName: "helloword.db", 16 | execSql: execQql, 17 | logger: logger.New( 18 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer 19 | logger.Config{ 20 | SlowThreshold: time.Second, // Slow SQL threshold 21 | LogLevel: logger.Silent, // Log level 22 | IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger 23 | Colorful: false, // Disable color 24 | }, 25 | ), 26 | } 27 | -------------------------------------------------------------------------------- /db/sqlite/settings.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | 11 | "github.com/mitchellh/go-homedir" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/wuqinqiang/helloword/logging" 15 | "gorm.io/driver/sqlite" 16 | "gorm.io/gorm" 17 | "gorm.io/gorm/logger" 18 | ) 19 | 20 | var ( 21 | //go:embed helloword.sql 22 | execQql string 23 | ) 24 | 25 | type Settings struct { 26 | //the db saved path 27 | path string 28 | // db fileName 29 | dbFileName string 30 | // sql for sqlite 31 | execSql string 32 | maxLifetime int 33 | maxIdleConns int 34 | maxOpenConns int 35 | // logger 36 | logger logger.Interface 37 | } 38 | 39 | func New(path string) *Settings { 40 | settings := DefaultSettings 41 | if path == "" { 42 | path = "~/.helloword" 43 | } 44 | settings.path = path 45 | return settings 46 | } 47 | 48 | func (settings *Settings) Init() error { 49 | return settings.init() 50 | } 51 | 52 | func (settings *Settings) init() error { 53 | // if default path have '~' 54 | actualPath, err := homedir.Expand(settings.path) 55 | if err != nil { 56 | return err 57 | } 58 | settings.path = actualPath 59 | _, err = os.Stat(filepath.Join(settings.path, settings.dbFileName)) 60 | noexist := os.IsNotExist(err) 61 | if err != nil && !noexist { 62 | return err 63 | } 64 | if noexist { 65 | err = os.MkdirAll(settings.path, 0755) //nolint: gosec 66 | if err != nil && !os.IsExist(err) { 67 | return err 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | func (settings *Settings) GetDb() (*gorm.DB, error) { 74 | gormDb, err := gorm.Open(sqlite.Open(filepath.Join(settings.path, settings.dbFileName+"?cache=shared")), &gorm.Config{ 75 | Logger: settings.logger, 76 | }) 77 | if err != nil { 78 | panic(err) 79 | } 80 | sqlDb, err := gormDb.DB() 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | if settings.execSql != "" { 86 | _, err = sqlDb.ExecContext(context.Background(), settings.execSql) 87 | if err != nil { 88 | //hard code for duplicate column 89 | if !strings.Contains(err.Error(), "duplicate column name") { 90 | return nil, errors.Wrap(err, "ExecContext") 91 | } 92 | logging.Warnf("[ExecContext] err:%v", err) 93 | } 94 | } 95 | sqlDb.SetMaxOpenConns(settings.maxOpenConns) 96 | sqlDb.SetMaxIdleConns(settings.maxIdleConns) 97 | sqlDb.SetConnMaxLifetime(time.Duration(settings.maxLifetime) * time.Second) 98 | return gormDb, nil 99 | } 100 | -------------------------------------------------------------------------------- /games/chain.go: -------------------------------------------------------------------------------- 1 | package games 2 | 3 | import ( 4 | "github.com/wuqinqiang/helloword/dao/model" 5 | ) 6 | 7 | type WordChain struct { 8 | // dataSets [letter]->words etc. [a]{apple,apply,application} 9 | dataSets map[string]model.Words 10 | // save words that playing games 11 | tmp map[string]struct{} 12 | //max tries for pick the word 13 | maxRetries int 14 | 15 | startWord *model.Word 16 | 17 | prevWord *model.Word 18 | } 19 | 20 | func NewWordChain(words model.Words, startWord *model.Word) *WordChain { 21 | wc := &WordChain{ 22 | dataSets: words.ListByLetter(), 23 | tmp: make(map[string]struct{}), 24 | startWord: startWord, 25 | prevWord: startWord, 26 | maxRetries: 5, 27 | } 28 | wc.tmp[startWord.Word] = struct{}{} 29 | return wc 30 | } 31 | 32 | func (chain *WordChain) SetPrevWord(word string) bool { 33 | _, ok := chain.tmp[word] 34 | if ok { 35 | return false 36 | } 37 | chain.prevWord = model.NewWord(word) 38 | chain.tmp[word] = struct{}{} 39 | 40 | return true 41 | } 42 | 43 | func (chain *WordChain) StartWord() *model.Word { 44 | return chain.startWord 45 | } 46 | 47 | func (chain *WordChain) PrevWord() *model.Word { 48 | return chain.prevWord 49 | } 50 | 51 | func (chain *WordChain) Pick() (*model.Word, bool) { 52 | letterWords, ok := chain.dataSets[chain.prevWord.Word[len(chain.prevWord.Word)-1:]] 53 | if !ok { 54 | return nil, false 55 | } 56 | 57 | for i := 0; i < chain.maxRetries; i++ { 58 | w := letterWords.RandomPick() 59 | 60 | if chain.SetPrevWord(w.Word) { 61 | return w, true 62 | } 63 | } 64 | 65 | return nil, false 66 | } 67 | -------------------------------------------------------------------------------- /games/chain_test.go: -------------------------------------------------------------------------------- 1 | package games 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wuqinqiang/helloword/dao/model" 7 | ) 8 | 9 | func TestWordChain(t *testing.T) { 10 | words := model.Words{ 11 | model.NewWord("apple"), 12 | model.NewWord("youngster"), 13 | model.NewWord("ear"), 14 | model.NewWord("art"), 15 | model.NewWord("eraser"), 16 | model.NewWord("rival"), 17 | model.NewWord("luxury"), 18 | } 19 | 20 | startWord := model.NewWord("apple") 21 | chain := NewWordChain(words, startWord) 22 | 23 | // Test if startWord is set correctly 24 | if chain.StartWord().Word != startWord.Word { 25 | t.Errorf("Start word is not set correctly") 26 | } 27 | 28 | // Test if the first pick is correct 29 | nextWord, ok := chain.Pick() 30 | if !ok { 31 | t.Errorf("Failed to pick next word") 32 | } else if !(nextWord.Word == "ear" || nextWord.Word == "eraser") { 33 | t.Errorf("Unexpected word picked") 34 | } 35 | 36 | // Test if the second pick is correct 37 | nextWord, ok = chain.Pick() 38 | if !ok { 39 | t.Errorf("Failed to pick next word") 40 | } else if nextWord.Word != "rival" { 41 | t.Errorf("Unexpected word picked") 42 | } 43 | 44 | // Test if the third pick is correct 45 | nextWord, ok = chain.Pick() 46 | if !ok { 47 | t.Errorf("Failed to pick next word") 48 | } else if nextWord.Word != "luxury" { 49 | t.Errorf("Unexpected word picked") 50 | } 51 | 52 | // Test if the fourth pick is correct 53 | nextWord, ok = chain.Pick() 54 | if !ok { 55 | t.Errorf("Failed to pick next word") 56 | } else if nextWord.Word != "youngster" { 57 | t.Errorf("Unexpected word picked") 58 | } 59 | 60 | // Test if the last pick returns false as expected 61 | // rival was used 62 | _, ok = chain.Pick() 63 | if ok { 64 | t.Errorf("Expected last pick to return false") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import "context" 4 | 5 | type Generator interface { 6 | Generate(ctx context.Context, words []string) (phrase string, err error) 7 | } 8 | -------------------------------------------------------------------------------- /generator/gpt3/client.go: -------------------------------------------------------------------------------- 1 | package gpt3 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "time" 11 | 12 | gogpt "github.com/sashabaranov/go-gpt3" 13 | ) 14 | 15 | var prompt = "请使用单词:%s 写一篇120字左右的英语短文。必须单独标注 %s 单词中文意思,同时把短文翻译成中文" 16 | 17 | type Client struct { 18 | *gogpt.Client 19 | } 20 | 21 | func NewClient(token string, proxyUrl string) (*Client, error) { 22 | conf := gogpt.DefaultConfig(token) 23 | 24 | if proxyUrl != "" { 25 | proxy, err := url.Parse(proxyUrl) 26 | if err != nil { 27 | return nil, err 28 | } 29 | dialer := net.Dialer{ 30 | Timeout: 30 * time.Second, 31 | } 32 | transport := &http.Transport{ 33 | Proxy: http.ProxyURL(proxy), 34 | DialContext: dialer.DialContext, 35 | MaxIdleConns: 50, 36 | IdleConnTimeout: 60 * time.Second, 37 | } 38 | conf.HTTPClient.Transport = transport 39 | } 40 | gpt := gogpt.NewClientWithConfig(conf) 41 | return &Client{gpt}, nil 42 | } 43 | 44 | func (client *Client) Generate(ctx context.Context, words []string) (phrase string, err error) { 45 | wordStr := strings.Join(words, ",") 46 | return client.request(ctx, fmt.Sprintf(prompt, wordStr, wordStr)) 47 | } 48 | 49 | func (client *Client) request(ctx context.Context, text string) (string, error) { 50 | 51 | req := gogpt.ChatCompletionRequest{ 52 | Model: gogpt.GPT3Dot5Turbo, 53 | Messages: []gogpt.ChatCompletionMessage{ 54 | { 55 | Role: "user", 56 | Content: text, 57 | }, 58 | }, 59 | MaxTokens: 2000, 60 | } 61 | 62 | var ( 63 | resp gogpt.ChatCompletionResponse 64 | err error 65 | ) 66 | 67 | for i := 0; i < 3; i++ { 68 | resp, err = client.CreateChatCompletion(ctx, req) 69 | if err == nil { 70 | if len(resp.Choices) == 0 { 71 | return "", err 72 | } 73 | fmt.Println("Generate:", resp.Choices[0].Message.Content) 74 | return resp.Choices[0].Message.Content, nil 75 | } 76 | } 77 | 78 | return "", err 79 | } 80 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wuqinqiang/helloword 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-resty/resty/v2 v2.7.0 7 | github.com/google/uuid v1.3.0 8 | github.com/mitchellh/go-homedir v1.1.0 9 | github.com/pkg/errors v0.9.1 10 | github.com/robfig/cron/v3 v3.0.0 11 | github.com/sashabaranov/go-gpt3 v1.3.1 12 | github.com/stretchr/testify v1.8.0 13 | github.com/urfave/cli/v2 v2.24.4 14 | go.uber.org/zap v1.24.0 15 | gopkg.in/yaml.v3 v3.0.1 16 | gorm.io/driver/sqlite v1.4.3 17 | gorm.io/gen v0.3.21 18 | gorm.io/gorm v1.24.5 19 | gorm.io/plugin/dbresolver v1.4.1 20 | ) 21 | 22 | require ( 23 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/go-sql-driver/mysql v1.7.0 // indirect 26 | github.com/jinzhu/inflection v1.0.0 // indirect 27 | github.com/jinzhu/now v1.1.5 // indirect 28 | github.com/mattn/go-sqlite3 v1.14.15 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 31 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 32 | go.uber.org/atomic v1.7.0 // indirect 33 | go.uber.org/multierr v1.6.0 // indirect 34 | golang.org/x/mod v0.8.0 // indirect 35 | golang.org/x/net v0.8.0 // indirect 36 | golang.org/x/sys v0.6.0 // indirect 37 | golang.org/x/tools v0.6.0 // indirect 38 | gorm.io/datatypes v1.1.0 // indirect 39 | gorm.io/driver/mysql v1.4.6 // indirect 40 | gorm.io/hints v1.1.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= 8 | github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= 9 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 10 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 11 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 12 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 13 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 14 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 15 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 16 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 17 | github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= 18 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 19 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 20 | github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= 21 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 22 | github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= 23 | github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= 24 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 25 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 26 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 27 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 28 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 29 | github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= 30 | github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 31 | github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= 32 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 33 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 34 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 35 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 36 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 38 | github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= 39 | github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 40 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 41 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 42 | github.com/sashabaranov/go-gpt3 v1.3.1 h1:ACQOAVX5CAV5rHt0oJOBMKo9BNcqVnmxEdjVxcjVAzw= 43 | github.com/sashabaranov/go-gpt3 v1.3.1/go.mod h1:BIZdbwdzxZbCrcKGMGH6u2eyGe1xFuX9Anmh3tCP8lQ= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 46 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 47 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 48 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 49 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 50 | github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU= 51 | github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 52 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 53 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 54 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 55 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 56 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 57 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 58 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 59 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 60 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 61 | golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= 62 | golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= 63 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 64 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 65 | golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= 66 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 67 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 68 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 69 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 70 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 73 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 75 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 77 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 78 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 79 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 80 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 81 | golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= 82 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 84 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 85 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 87 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | gorm.io/datatypes v1.1.0 h1:EVp1Z28N4ACpYFK1nHboEIJGIFfjY7vLeieDk8jSHJA= 89 | gorm.io/datatypes v1.1.0/go.mod h1:SH2K9R+2RMjuX1CkCONrPwoe9JzVv2hkQvEu4bXGojE= 90 | gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= 91 | gorm.io/driver/mysql v1.4.6 h1:5zS3vIKcyb46byXZNcYxaT9EWNIhXzu0gPuvvVrwZ8s= 92 | gorm.io/driver/mysql v1.4.6/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc= 93 | gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc= 94 | gorm.io/driver/sqlite v1.4.2/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= 95 | gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= 96 | gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= 97 | gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= 98 | gorm.io/gen v0.3.21 h1:t8329wT4tW1ZZWOm7vn4LV6OIrz8a5zCg+p78ezt+rA= 99 | gorm.io/gen v0.3.21/go.mod h1:aWgvoKdG9f8Des4TegSa0N5a+gwhGsFo0JJMaLwokvk= 100 | gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 101 | gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 102 | gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 103 | gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE= 104 | gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 105 | gorm.io/hints v1.1.1 h1:NPampLxQujY+277452rt4yqtg6JmzNZ1jA2olk0eFXw= 106 | gorm.io/hints v1.1.1/go.mod h1:zdwzfFqvBWGbpuKiAhLFOSGSpeD3/VsRgkXR9Y7Z3cs= 107 | gorm.io/plugin/dbresolver v1.4.1 h1:Ug4LcoPhrvqq71UhxtF346f+skTYoCa/nEsdjvHwEzk= 108 | gorm.io/plugin/dbresolver v1.4.1/go.mod h1:CTbCtMWhsjXSiJqiW2R8POvJ2cq18RVOl4WGyT5nhNc= 109 | -------------------------------------------------------------------------------- /library/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuqinqiang/helloword/37eb1adba37e115cb98634eb381e2c67835b70db/library/example.png -------------------------------------------------------------------------------- /library/word_chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuqinqiang/helloword/37eb1adba37e115cb98634eb381e2c67835b70db/library/word_chain.png -------------------------------------------------------------------------------- /logging/helper.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "runtime" 7 | 8 | "go.uber.org/zap/zapcore" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var ( 14 | logger *zap.Logger 15 | ) 16 | 17 | func Infof(format string, keyVals ...interface{}) { 18 | log(zapcore.InfoLevel, fmt.Sprintf(format, keyVals...)) 19 | } 20 | 21 | func Errorf(format string, keyVals ...interface{}) { 22 | log(zapcore.ErrorLevel, fmt.Sprintf(format, keyVals...)) 23 | } 24 | 25 | func Warnf(format string, keyVals ...interface{}) { 26 | log(zapcore.WarnLevel, fmt.Sprintf(format, keyVals...)) 27 | } 28 | 29 | func Debugf(format string, keyVals ...interface{}) { 30 | log(zapcore.DebugLevel, fmt.Sprintf(format, keyVals...)) 31 | } 32 | 33 | func log(level zapcore.Level, msg string, keyvals ...interface{}) { 34 | if len(keyvals)%2 != 0 { 35 | logger.Warn(fmt.Sprintf("Keyvalues must appear in pairs:%v", keyvals)) 36 | return 37 | } 38 | 39 | var ( 40 | fields []zap.Field 41 | ) 42 | 43 | if level != zapcore.InfoLevel { 44 | fields = append(fields, getCallerInfoForLog()...) 45 | } 46 | 47 | for i := 0; i < len(keyvals); i += 2 { 48 | fields = append(fields, zap.Any(fmt.Sprint(keyvals[i]), keyvals[i+1])) 49 | } 50 | switch level { 51 | case zapcore.InfoLevel: 52 | logger.Info(msg, fields...) 53 | case zapcore.DebugLevel: 54 | logger.Debug(msg, fields...) 55 | case zapcore.FatalLevel: 56 | logger.Fatal(msg, fields...) 57 | case zapcore.ErrorLevel: 58 | logger.Error(msg, fields...) 59 | case zapcore.WarnLevel: 60 | logger.Warn(msg, fields...) 61 | default: 62 | logger.Warn(fmt.Sprintf("logging not included level:%v", level)) 63 | } 64 | } 65 | 66 | func getCallerInfoForLog() (callerFields []zap.Field) { 67 | pc, file, line, ok := runtime.Caller(3) 68 | if !ok { 69 | return 70 | } 71 | funcName := runtime.FuncForPC(pc).Name() 72 | funcName = path.Base(funcName) 73 | 74 | callerFields = append(callerFields, zap.String("func", funcName), zap.String("file", file), zap.Int("line", line)) 75 | return 76 | } 77 | -------------------------------------------------------------------------------- /logging/log.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "os" 5 | 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | func init() { 11 | encoderConfig := zap.NewProductionEncoderConfig() 12 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 13 | encoder := zapcore.NewJSONEncoder(encoderConfig) 14 | core := zapcore.NewTee( 15 | zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel), 16 | ) 17 | logger = zap.New(core) 18 | } 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/wuqinqiang/helloword/db" 8 | 9 | "github.com/wuqinqiang/helloword/db/sqlite" 10 | 11 | "github.com/urfave/cli/v2" 12 | "github.com/wuqinqiang/helloword/cmd" 13 | ) 14 | 15 | func main() { 16 | app := &cli.App{ 17 | Name: "hello word", 18 | Usage: "happy study english word", 19 | Before: func(context *cli.Context) error { 20 | dbPath := strings.TrimSpace(os.Getenv("HELLO_WORD_PATH")) 21 | if err := db.Init(sqlite.New(dbPath)); err != nil { 22 | return err 23 | } 24 | return nil 25 | }, 26 | Commands: []*cli.Command{ 27 | cmd.DaemonCmd, 28 | cmd.ImportCmd, 29 | cmd.GenCmd, 30 | cmd.PhraseCmd, 31 | cmd.GamesCmd, 32 | }, 33 | } 34 | app.Setup() 35 | if err := app.Run(os.Args); err != nil { 36 | os.Stderr.WriteString("Error:" + err.Error() + "\n") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /notify/base/subject.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | type Subject struct { 4 | title string 5 | text string 6 | } 7 | 8 | func New(title string, text string) Subject { 9 | return Subject{ 10 | title: title, 11 | text: text, 12 | } 13 | } 14 | 15 | func (subject *Subject) Text() string { 16 | return subject.text 17 | } 18 | 19 | func (subject *Subject) Title() string { 20 | return subject.title 21 | } 22 | -------------------------------------------------------------------------------- /notify/dingtalk/dingtalk.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "github.com/wuqinqiang/helloword/notify/base" 10 | "net/url" 11 | "time" 12 | 13 | . "github.com/wuqinqiang/helloword/tools" 14 | ) 15 | 16 | // NotifyConfig is the dingtalk notification configuration 17 | type NotifyConfig struct { 18 | WebhookURL string `yaml:"webhook"` 19 | SignSecret string `yaml:"secret,omitempty"` 20 | } 21 | 22 | // Send will post to an 'Robot Webhook' url in Dingtalk Apps. It accepts 23 | // some text and the Dingtalk robot will send it in group. 24 | func (c NotifyConfig) Send(subject base.Subject) error { 25 | title := "**" + subject.Title() + "**" 26 | // It will be better to escape the msg. 27 | msgContent := fmt.Sprintf(` 28 | { 29 | "msgtype": "markdown", 30 | "markdown": { 31 | "title": "%s", 32 | "text": "%s" 33 | } 34 | } 35 | `, title, subject.Text()) 36 | 37 | resp, err := Resty.SetTimeout(5*time.Second).SetRetryCount(3).R(). 38 | SetHeader("Content-Type", "application/json"). 39 | SetBody(msgContent).Post(c.addSign(c.WebhookURL, c.SignSecret)) 40 | if err != nil { 41 | return err 42 | } 43 | ret := make(map[string]interface{}) 44 | err = json.Unmarshal(resp.Body(), &ret) 45 | if err != nil || ret["errmsg"] != "ok" { 46 | return fmt.Errorf("error response from Dingtalk [%d] - [%s]", resp.StatusCode(), string(resp.Body())) 47 | } 48 | return nil 49 | } 50 | 51 | // add sign for url by secret 52 | func (c NotifyConfig) addSign(webhookURL string, secret string) string { 53 | webhook := webhookURL 54 | if secret != "" { 55 | timestamp := time.Now().UnixMilli() 56 | stringToSign := fmt.Sprint(timestamp, "\n", secret) 57 | h := hmac.New(sha256.New, []byte(secret)) 58 | h.Write([]byte(stringToSign)) 59 | sign := url.QueryEscape(base64.StdEncoding.EncodeToString(h.Sum(nil))) 60 | webhook = fmt.Sprint(webhookURL, "×tamp=", timestamp, "&sign="+sign) 61 | } 62 | return webhook 63 | } 64 | -------------------------------------------------------------------------------- /notify/lark/lark.go: -------------------------------------------------------------------------------- 1 | package lark 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/wuqinqiang/helloword/notify/base" 7 | "time" 8 | 9 | . "github.com/wuqinqiang/helloword/tools" 10 | ) 11 | 12 | // NotifyConfig is the lark notification configuration 13 | type NotifyConfig struct { 14 | WebhookURL string `yaml:"webhook"` 15 | } 16 | 17 | // Send is the wrapper for SendLarkNotification 18 | func (c NotifyConfig) Send(subject base.Subject) error { 19 | return c.SendLarkNotification(subject) 20 | } 21 | 22 | // SendLarkNotification will post to an 'Robot Webhook' url in Lark Apps. It accepts 23 | // some text and the Lark robot will send it in group. 24 | func (c NotifyConfig) SendLarkNotification(subject base.Subject) error { 25 | b := body{ 26 | MsgType: "text", 27 | } 28 | b.Context.Text = subject.Text() 29 | 30 | resp, err := Resty.SetTimeout(5*time.Second).SetRetryCount(3).R(). 31 | SetHeader("Content-Type", "application/json"). 32 | SetBody(b).Post(c.WebhookURL) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | ret := make(map[string]interface{}) 38 | err = json.Unmarshal(resp.Body(), &ret) 39 | if err != nil { 40 | return fmt.Errorf("error response from Lark [%d] - [%s]", resp.StatusCode(), string(resp.Body())) 41 | } 42 | // Server returns {"Extra":null,"StatusCode":0,"StatusMessage":"success"} on success 43 | // otherwise it returns {"code":9499,"msg":"Bad Request","data":{}} 44 | if statusCode, ok := ret["StatusCode"].(float64); !ok || statusCode != 0 { 45 | code, _ := ret["code"].(float64) 46 | msg, _ := ret["msg"].(string) 47 | return fmt.Errorf("error response from Lark - code [%d] - msg [%v]", int(code), msg) 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /notify/lark/struct.go: -------------------------------------------------------------------------------- 1 | package lark 2 | 3 | type body struct { 4 | MsgType string `json:"msg_type"` 5 | Context context `json:"content"` 6 | } 7 | 8 | type context struct { 9 | Text string `json:"text"` 10 | } 11 | -------------------------------------------------------------------------------- /notify/notify.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/wuqinqiang/helloword/notify/base" 9 | 10 | "github.com/wuqinqiang/helloword/tools" 11 | ) 12 | 13 | type Notify interface { 14 | Notify(subject base.Subject) 15 | Stop() 16 | Wait() 17 | } 18 | 19 | type Sender interface { 20 | Send(subject base.Subject) error 21 | } 22 | 23 | type notify struct { 24 | ctx context.Context 25 | cancel func() 26 | senders []Sender 27 | ch chan base.Subject 28 | wg sync.WaitGroup 29 | } 30 | 31 | func New(senders []Sender) Notify { 32 | n := ¬ify{ 33 | senders: senders, 34 | ch: make(chan base.Subject, 50), 35 | } 36 | n.ctx, n.cancel = context.WithCancel(context.Background()) 37 | go n.waitEvent() 38 | return n 39 | } 40 | 41 | func (n *notify) Notify(subject base.Subject) { 42 | if len(n.senders) == 0 { 43 | return 44 | } 45 | n.ch <- subject 46 | n.wg.Add(len(n.senders)) 47 | } 48 | 49 | func (n *notify) Stop() { 50 | n.cancel() 51 | } 52 | 53 | func (n *notify) Wait() { 54 | n.wg.Wait() 55 | } 56 | 57 | func (n *notify) waitEvent() { 58 | for { 59 | select { 60 | case <-n.ctx.Done(): 61 | return 62 | case subject := <-n.ch: 63 | for _, sender := range n.senders { 64 | tools.GoSafe(func() { 65 | defer n.wg.Done() 66 | err := sender.Send(subject) 67 | if err != nil { 68 | fmt.Println() 69 | } 70 | }) 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /notify/telegram/telegram.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "time" 7 | 8 | "github.com/wuqinqiang/helloword/notify/base" 9 | 10 | . "github.com/wuqinqiang/helloword/tools" 11 | ) 12 | 13 | // NotifyConfig is the telegram notification configuration 14 | type NotifyConfig struct { 15 | Token string `yaml:"token"` 16 | ChatID string `yaml:"chat_id"` 17 | } 18 | 19 | // Send is the wrapper for SendTelegramNotification 20 | func (c NotifyConfig) Send(subject base.Subject) error { 21 | return c.SendTelegramNotification(subject) 22 | } 23 | 24 | // SendTelegramNotification will send the notification to telegram. 25 | func (c NotifyConfig) SendTelegramNotification(subject base.Subject) error { 26 | api := "https://api.telegram.org/bot" + c.Token + 27 | "/sendMessage?&chat_id=" + c.ChatID + 28 | "&parse_mode=markdown" + 29 | "&text=" + url.QueryEscape(subject.Text()) 30 | 31 | resp, err := Resty.SetTimeout(5*time.Second).SetRetryCount(3).R(). 32 | SetHeader("Content-Type", "application/json"). 33 | Post(api) 34 | if err != nil { 35 | return err 36 | } 37 | if resp.StatusCode() != 200 { 38 | return fmt.Errorf("error response from Telegram - code [%d]", resp.StatusCode()) 39 | } 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /selector/options.go: -------------------------------------------------------------------------------- 1 | package selector 2 | 3 | type Option func(srv *Srv) 4 | 5 | func WithWordNumber(wordNumer int) Option { 6 | return func(srv *Srv) { 7 | srv.wordNumber = wordNumer 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /selector/selector.go: -------------------------------------------------------------------------------- 1 | package selector 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wuqinqiang/helloword/dao" 7 | "github.com/wuqinqiang/helloword/dao/model" 8 | s "github.com/wuqinqiang/helloword/selector/strategy" 9 | ) 10 | 11 | type StrategyType string 12 | 13 | const ( 14 | Default StrategyType = "default" 15 | Random StrategyType = "random" 16 | LeastRecentlyUsed StrategyType = "leastRecentlyUsed" 17 | 18 | MinWordsNum = 5 19 | MaxWordsNum = 10 20 | ) 21 | 22 | type Selector interface { 23 | NextWords(ctx context.Context) (words model.Words, err error) 24 | SetStrategyType(strategy Strategy) 25 | } 26 | type Strategy interface { 27 | Select(words model.Words) model.Words 28 | } 29 | 30 | type Srv struct { 31 | dao.Dao 32 | wordNumber int 33 | strategy Strategy 34 | } 35 | 36 | func New(strategyType StrategyType, options ...Option) Selector { 37 | srv := &Srv{ 38 | Dao: dao.Get(), 39 | } 40 | for _, option := range options { 41 | option(srv) 42 | } 43 | 44 | if srv.wordNumber <= 0 { 45 | srv.wordNumber = MinWordsNum 46 | } 47 | if srv.wordNumber > MaxWordsNum { 48 | srv.wordNumber = MaxWordsNum 49 | } 50 | 51 | var strategy Strategy 52 | switch strategyType { 53 | case LeastRecentlyUsed: 54 | strategy = s.NewLeastRecentlyUsed() 55 | case Default: 56 | default: 57 | strategy = s.NewRandom() 58 | } 59 | 60 | srv.strategy = strategy 61 | return srv 62 | } 63 | 64 | func (s *Srv) NextWords(ctx context.Context) (words model.Words, err error) { 65 | var list model.Words 66 | list, err = s.Word.GetList(ctx) 67 | if err != nil { 68 | return 69 | } 70 | 71 | words = s.strategy.Select(list) 72 | // not enough, then all out 73 | if len(words) < s.wordNumber { 74 | return 75 | } 76 | words = words[:s.wordNumber] 77 | 78 | return 79 | } 80 | 81 | func (s *Srv) SetStrategyType(strategy Strategy) { 82 | s.strategy = strategy 83 | } 84 | -------------------------------------------------------------------------------- /selector/strategy/least_recently_used.go: -------------------------------------------------------------------------------- 1 | package strategy 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/wuqinqiang/helloword/dao/model" 7 | ) 8 | 9 | type LeastRecentlyUsed struct{} 10 | 11 | func NewLeastRecentlyUsed() *LeastRecentlyUsed { 12 | return &LeastRecentlyUsed{} 13 | } 14 | 15 | func (l LeastRecentlyUsed) Select(words model.Words) model.Words { 16 | // sort the words by NumRepetitions field in ascending order 17 | sort.Slice(words, func(i, j int) bool { 18 | return words[i].NumRepetitions < words[j].NumRepetitions 19 | }) 20 | 21 | // sort the words by last_used field in ascending order 22 | sort.Slice(words, func(i, j int) bool { 23 | return words[i].LastUsed < words[j].LastUsed 24 | }) 25 | return words 26 | } 27 | -------------------------------------------------------------------------------- /selector/strategy/random.go: -------------------------------------------------------------------------------- 1 | package strategy 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/wuqinqiang/helloword/dao/model" 8 | ) 9 | 10 | type RandomStrategy struct{} 11 | 12 | func NewRandom() *RandomStrategy { 13 | return &RandomStrategy{} 14 | } 15 | 16 | func (s *RandomStrategy) Select(words model.Words) model.Words { 17 | shuffled := make(model.Words, len(words)) 18 | copy(shuffled, words) 19 | 20 | rand.Seed(time.Now().UnixNano()) 21 | rand.Shuffle(len(shuffled), func(i, j int) { 22 | shuffled[i], shuffled[j] = shuffled[j], shuffled[i] 23 | }) 24 | return shuffled 25 | } 26 | -------------------------------------------------------------------------------- /tools/fx/README.md: -------------------------------------------------------------------------------- 1 | CONY FROM go-zero !!!!!! -------------------------------------------------------------------------------- /tools/fx/fn.go: -------------------------------------------------------------------------------- 1 | package fx 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | 7 | "github.com/wuqinqiang/helloword/tools" 8 | ) 9 | 10 | const ( 11 | defaultWorkers = 16 12 | minWorkers = 1 13 | ) 14 | 15 | type ( 16 | rxOptions struct { 17 | unlimitedWorkers bool 18 | workers int 19 | } 20 | 21 | FilterFunc func(item interface{}) bool 22 | ForAllFunc func(pipe <-chan interface{}) 23 | ForEachFunc func(item interface{}) 24 | GenerateFunc func(source chan<- interface{}) 25 | KeyFunc func(item interface{}) interface{} 26 | LessFunc func(a, b interface{}) bool 27 | MapFunc func(item interface{}) interface{} 28 | Option func(opts *rxOptions) 29 | ParallelFunc func(item interface{}) 30 | ReduceFunc func(pipe <-chan interface{}) (interface{}, error) 31 | WalkFunc func(item interface{}, pipe chan<- interface{}) 32 | 33 | Stream struct { 34 | source <-chan interface{} 35 | } 36 | ) 37 | 38 | // From constructs a Stream from the given GenerateFunc. 39 | func From(generate GenerateFunc) Stream { 40 | source := make(chan interface{}) 41 | 42 | tools.GoSafe(func() { 43 | defer close(source) 44 | // 构造流数据写入channel 45 | generate(source) 46 | }) 47 | 48 | return Range(source) 49 | } 50 | 51 | // Just converts the given arbitrary items to a Stream. 52 | func Just(items ...interface{}) Stream { 53 | source := make(chan interface{}, len(items)) 54 | for _, item := range items { 55 | source <- item 56 | } 57 | close(source) 58 | 59 | return Range(source) 60 | } 61 | 62 | // Range converts the given channel to a Stream. 63 | func Range(source <-chan interface{}) Stream { 64 | return Stream{ 65 | source: source, 66 | } 67 | } 68 | 69 | // Buffer buffers the items into a queue with size n. 70 | // It can balance the producer and the consumer if their processing throughput don't match. 71 | func (p Stream) Buffer(n int) Stream { 72 | if n < 0 { 73 | n = 0 74 | } 75 | 76 | source := make(chan interface{}, n) 77 | go func() { 78 | for item := range p.source { 79 | source <- item 80 | } 81 | close(source) 82 | }() 83 | 84 | return Range(source) 85 | } 86 | 87 | // Count counts the number of elements in the result. 88 | func (p Stream) Count() (count int) { 89 | for range p.source { 90 | count++ 91 | } 92 | return 93 | } 94 | 95 | // Distinct removes the duplicated items base on the given KeyFunc. 96 | func (p Stream) Distinct(fn KeyFunc) Stream { 97 | source := make(chan interface{}) 98 | 99 | tools.GoSafe(func() { 100 | defer close(source) 101 | 102 | keys := make(map[interface{}]struct{}) 103 | for item := range p.source { 104 | key := fn(item) 105 | if _, ok := keys[key]; !ok { 106 | source <- item 107 | keys[key] = struct{}{} 108 | } 109 | } 110 | }) 111 | 112 | return Range(source) 113 | } 114 | 115 | // Done waits all upstreaming operations to be done. 116 | func (p Stream) Done() { 117 | for range p.source { 118 | } 119 | } 120 | 121 | // Filter filters the items by the given FilterFunc. 122 | func (p Stream) Filter(fn FilterFunc, opts ...Option) Stream { 123 | return p.Walk(func(item interface{}, pipe chan<- interface{}) { 124 | if fn(item) { 125 | pipe <- item 126 | } 127 | }, opts...) 128 | } 129 | 130 | // ForAll handles the streaming elements from the source and no later streams. 131 | func (p Stream) ForAll(fn ForAllFunc) { 132 | fn(p.source) 133 | } 134 | 135 | // ForEach seals the Stream with the ForEachFunc on each item, no successive operations. 136 | func (p Stream) ForEach(fn ForEachFunc) { 137 | for item := range p.source { 138 | fn(item) 139 | } 140 | } 141 | 142 | // Group groups the elements into different groups based on their keys. 143 | func (p Stream) Group(fn KeyFunc) Stream { 144 | groups := make(map[interface{}][]interface{}) 145 | for item := range p.source { 146 | key := fn(item) 147 | groups[key] = append(groups[key], item) 148 | } 149 | 150 | source := make(chan interface{}) 151 | go func() { 152 | for _, group := range groups { 153 | source <- group 154 | } 155 | close(source) 156 | }() 157 | 158 | return Range(source) 159 | } 160 | 161 | func (p Stream) Head(n int64) Stream { 162 | if n < 1 { 163 | panic("n must be greater than 0") 164 | } 165 | 166 | source := make(chan interface{}) 167 | 168 | go func() { 169 | for item := range p.source { 170 | n-- 171 | if n >= 0 { 172 | source <- item 173 | } 174 | if n == 0 { 175 | // let successive method go ASAP even we have more items to skip 176 | // why we don't just break the loop, because if break, 177 | // this former threading will block forever, which will cause threading leak. 178 | close(source) 179 | } 180 | } 181 | if n > 0 { 182 | close(source) 183 | } 184 | }() 185 | 186 | return Range(source) 187 | } 188 | 189 | // Maps converts each item to another corresponding item, which means it's a 1:1 model. 190 | func (p Stream) Map(fn MapFunc, opts ...Option) Stream { 191 | return p.Walk(func(item interface{}, pipe chan<- interface{}) { 192 | pipe <- fn(item) 193 | }, opts...) 194 | } 195 | 196 | // Merge merges all the items into a slice and generates a new stream. 197 | func (p Stream) Merge() Stream { 198 | var items []interface{} 199 | for item := range p.source { 200 | items = append(items, item) 201 | } 202 | 203 | source := make(chan interface{}, 1) 204 | source <- items 205 | close(source) 206 | 207 | return Range(source) 208 | } 209 | 210 | // Parallel applies the given ParallelFunc to each item concurrently with given number of workers. 211 | func (p Stream) Parallel(fn ParallelFunc, opts ...Option) { 212 | p.Walk(func(item interface{}, pipe chan<- interface{}) { 213 | fn(item) 214 | }, opts...).Done() 215 | } 216 | 217 | // Reduce is a utility method to let the caller deal with the underlying channel. 218 | func (p Stream) Reduce(fn ReduceFunc) (interface{}, error) { 219 | return fn(p.source) 220 | } 221 | 222 | // Reverse reverses the elements in the stream. 223 | func (p Stream) Reverse() Stream { 224 | var items []interface{} 225 | for item := range p.source { 226 | items = append(items, item) 227 | } 228 | // reverse, official method 229 | for i := len(items)/2 - 1; i >= 0; i-- { 230 | opp := len(items) - 1 - i 231 | items[i], items[opp] = items[opp], items[i] 232 | } 233 | 234 | return Just(items...) 235 | } 236 | 237 | // Sort sorts the items from the underlying source. 238 | func (p Stream) Sort(less LessFunc) Stream { 239 | var items []interface{} 240 | for item := range p.source { 241 | items = append(items, item) 242 | } 243 | sort.Slice(items, func(i, j int) bool { 244 | return less(items[i], items[j]) 245 | }) 246 | 247 | return Just(items...) 248 | } 249 | 250 | // Split splits the elements into chunk with size up to n, 251 | // might be less than n on tailing elements. 252 | func (p Stream) Split(n int) Stream { 253 | if n < 1 { 254 | panic("n should be greater than 0") 255 | } 256 | 257 | source := make(chan interface{}) 258 | go func() { 259 | var chunk []interface{} 260 | for item := range p.source { 261 | chunk = append(chunk, item) 262 | if len(chunk) == n { 263 | source <- chunk 264 | chunk = nil 265 | } 266 | } 267 | if chunk != nil { 268 | source <- chunk 269 | } 270 | close(source) 271 | }() 272 | 273 | return Range(source) 274 | } 275 | 276 | func (p Stream) Tail(n int64) Stream { 277 | if n < 1 { 278 | panic("n should be greater than 0") 279 | } 280 | 281 | source := make(chan interface{}) 282 | 283 | go func() { 284 | ring := NewRing(int(n)) 285 | for item := range p.source { 286 | ring.Add(item) 287 | } 288 | for _, item := range ring.Take() { 289 | source <- item 290 | } 291 | close(source) 292 | }() 293 | 294 | return Range(source) 295 | } 296 | 297 | // Walk lets the callers handle each item, the caller may write zero, one or more items base on the given item. 298 | func (p Stream) Walk(fn WalkFunc, opts ...Option) Stream { 299 | option := buildOptions(opts...) 300 | if option.unlimitedWorkers { 301 | return p.walkUnlimited(fn) 302 | } else { 303 | return p.walkLimited(fn, option) 304 | } 305 | } 306 | 307 | func (p Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream { 308 | pipe := make(chan interface{}, option.workers) 309 | 310 | go func() { 311 | var wg sync.WaitGroup 312 | pool := make(chan struct{}, option.workers) 313 | for { 314 | pool <- struct{}{} 315 | item, ok := <-p.source 316 | if !ok { 317 | <-pool 318 | break 319 | } 320 | 321 | wg.Add(1) 322 | // better to safely run caller defined method 323 | tools.GoSafe(func() { 324 | defer func() { 325 | wg.Done() 326 | <-pool 327 | }() 328 | 329 | fn(item, pipe) 330 | }) 331 | } 332 | 333 | wg.Wait() 334 | close(pipe) 335 | }() 336 | 337 | return Range(pipe) 338 | } 339 | 340 | func (p Stream) walkUnlimited(fn WalkFunc) Stream { 341 | pipe := make(chan interface{}, defaultWorkers) 342 | 343 | go func() { 344 | var wg sync.WaitGroup 345 | 346 | for { 347 | item, ok := <-p.source 348 | if !ok { 349 | break 350 | } 351 | 352 | wg.Add(1) 353 | // better to safely run caller defined method 354 | tools.GoSafe(func() { 355 | defer wg.Done() 356 | fn(item, pipe) 357 | }) 358 | } 359 | 360 | wg.Wait() 361 | close(pipe) 362 | }() 363 | 364 | return Range(pipe) 365 | } 366 | 367 | // UnlimitedWorkers lets the caller to use as many workers as the tasks. 368 | func UnlimitedWorkers() Option { 369 | return func(opts *rxOptions) { 370 | opts.unlimitedWorkers = true 371 | } 372 | } 373 | 374 | // WithWorkers lets the caller to customize the concurrent workers. 375 | func WithWorkers(workers int) Option { 376 | return func(opts *rxOptions) { 377 | if workers < minWorkers { 378 | opts.workers = minWorkers 379 | } else { 380 | opts.workers = workers 381 | } 382 | } 383 | } 384 | 385 | func buildOptions(opts ...Option) *rxOptions { 386 | options := newOptions() 387 | for _, opt := range opts { 388 | opt(options) 389 | } 390 | 391 | return options 392 | } 393 | 394 | func newOptions() *rxOptions { 395 | return &rxOptions{ 396 | workers: defaultWorkers, 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /tools/fx/fx_test.go: -------------------------------------------------------------------------------- 1 | package fx 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "runtime" 7 | "sync" 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestBuffer(t *testing.T) { 16 | const N = 5 17 | var count int32 18 | var wait sync.WaitGroup 19 | wait.Add(1) 20 | From(func(source chan<- interface{}) { 21 | ticker := time.NewTicker(10 * time.Millisecond) 22 | defer ticker.Stop() 23 | 24 | for i := 0; i < 2*N; i++ { 25 | select { 26 | case source <- i: 27 | atomic.AddInt32(&count, 1) 28 | case <-ticker.C: 29 | wait.Done() 30 | return 31 | } 32 | } 33 | }).Buffer(N).ForAll(func(pipe <-chan interface{}) { 34 | wait.Wait() 35 | // why N+1, because take one more to wait for sending into the channel 36 | assert.Equal(t, int32(N+1), atomic.LoadInt32(&count)) 37 | }) 38 | } 39 | 40 | func TestBufferNegative(t *testing.T) { 41 | var result int 42 | _, err := Just(1, 2, 3, 4).Buffer(-1).Reduce(func(pipe <-chan interface{}) (interface{}, error) { 43 | for item := range pipe { 44 | result += item.(int) 45 | } 46 | return result, nil 47 | }) 48 | if err != nil { 49 | return 50 | } 51 | assert.Equal(t, 10, result) 52 | } 53 | 54 | func TestCount(t *testing.T) { 55 | tests := []struct { 56 | name string 57 | elements []interface{} 58 | }{ 59 | { 60 | name: "no elements with nil", 61 | }, 62 | { 63 | name: "no elements", 64 | elements: []interface{}{}, 65 | }, 66 | { 67 | name: "1 element", 68 | elements: []interface{}{1}, 69 | }, 70 | { 71 | name: "multiple elements", 72 | elements: []interface{}{1, 2, 3}, 73 | }, 74 | } 75 | 76 | for _, test := range tests { 77 | t.Run(test.name, func(t *testing.T) { 78 | val := Just(test.elements...).Count() 79 | assert.Equal(t, len(test.elements), val) 80 | }) 81 | } 82 | } 83 | 84 | func TestDone(t *testing.T) { 85 | var count int32 86 | Just(1, 2, 3).Walk(func(item interface{}, pipe chan<- interface{}) { 87 | time.Sleep(time.Millisecond * 100) 88 | atomic.AddInt32(&count, int32(item.(int))) 89 | }).Done() 90 | assert.Equal(t, int32(6), count) 91 | } 92 | 93 | func TestJust(t *testing.T) { 94 | var result int 95 | _, err := Just(1, 2, 3, 4).Reduce(func(pipe <-chan interface{}) (interface{}, error) { 96 | for item := range pipe { 97 | result += item.(int) 98 | } 99 | return result, nil 100 | }) 101 | if err != nil { 102 | return 103 | } 104 | assert.Equal(t, 10, result) 105 | } 106 | 107 | func TestDistinct(t *testing.T) { 108 | var result int 109 | _, err := Just(4, 1, 3, 2, 3, 4).Distinct(func(item interface{}) interface{} { 110 | return item 111 | }).Reduce(func(pipe <-chan interface{}) (interface{}, error) { 112 | for item := range pipe { 113 | result += item.(int) 114 | } 115 | return result, nil 116 | }) 117 | if err != nil { 118 | return 119 | } 120 | assert.Equal(t, 10, result) 121 | } 122 | 123 | func TestFilter(t *testing.T) { 124 | var result int 125 | _, err := Just(1, 2, 3, 4).Filter(func(item interface{}) bool { 126 | return item.(int)%2 == 0 127 | }).Reduce(func(pipe <-chan interface{}) (interface{}, error) { 128 | for item := range pipe { 129 | result += item.(int) 130 | } 131 | return result, nil 132 | }) 133 | if err != nil { 134 | return 135 | } 136 | assert.Equal(t, 6, result) 137 | } 138 | 139 | func TestForAll(t *testing.T) { 140 | var result int 141 | Just(1, 2, 3, 4).Filter(func(item interface{}) bool { 142 | return item.(int)%2 == 0 143 | }).ForAll(func(pipe <-chan interface{}) { 144 | for item := range pipe { 145 | result += item.(int) 146 | } 147 | }) 148 | assert.Equal(t, 6, result) 149 | } 150 | 151 | func TestGroup(t *testing.T) { 152 | var groups [][]int 153 | Just(10, 11, 20, 21).Group(func(item interface{}) interface{} { 154 | v := item.(int) 155 | return v / 10 156 | }).ForEach(func(item interface{}) { 157 | v := item.([]interface{}) 158 | var group []int 159 | for _, each := range v { 160 | group = append(group, each.(int)) 161 | } 162 | groups = append(groups, group) 163 | }) 164 | 165 | assert.Equal(t, 2, len(groups)) 166 | for _, group := range groups { 167 | assert.Equal(t, 2, len(group)) 168 | assert.True(t, group[0]/10 == group[1]/10) 169 | } 170 | } 171 | 172 | func TestHead(t *testing.T) { 173 | var result int 174 | _, err := Just(1, 2, 3, 4).Head(2).Reduce(func(pipe <-chan interface{}) (interface{}, error) { 175 | for item := range pipe { 176 | result += item.(int) 177 | } 178 | return result, nil 179 | }) 180 | if err != nil { 181 | return 182 | } 183 | assert.Equal(t, 3, result) 184 | } 185 | 186 | func TestHeadZero(t *testing.T) { 187 | assert.Panics(t, func() { 188 | _, err := Just(1, 2, 3, 4).Head(0).Reduce(func(pipe <-chan interface{}) (interface{}, error) { 189 | return nil, nil 190 | }) 191 | if err != nil { 192 | return 193 | } 194 | }) 195 | } 196 | 197 | func TestHeadMore(t *testing.T) { 198 | var result int 199 | _, err := Just(1, 2, 3, 4).Head(6).Reduce(func(pipe <-chan interface{}) (interface{}, error) { 200 | for item := range pipe { 201 | result += item.(int) 202 | } 203 | return result, nil 204 | }) 205 | if err != nil { 206 | return 207 | } 208 | assert.Equal(t, 10, result) 209 | } 210 | 211 | func TestMap(t *testing.T) { 212 | log.SetOutput(io.Discard) 213 | 214 | tests := []struct { 215 | mapper MapFunc 216 | expect int 217 | }{ 218 | { 219 | mapper: func(item interface{}) interface{} { 220 | v := item.(int) 221 | return v * v 222 | }, 223 | expect: 30, 224 | }, 225 | { 226 | mapper: func(item interface{}) interface{} { 227 | v := item.(int) 228 | if v%2 == 0 { 229 | return 0 230 | } 231 | return v * v 232 | }, 233 | expect: 10, 234 | }, 235 | { 236 | mapper: func(item interface{}) interface{} { 237 | v := item.(int) 238 | if v%2 == 0 { 239 | return 0 240 | } 241 | return v * v 242 | }, 243 | expect: 10, 244 | }, 245 | } 246 | 247 | // Map(...) works even WithWorkers(0) 248 | for i, test := range tests { 249 | t.Run("TestMap", func(t *testing.T) { 250 | var result int 251 | var workers int 252 | if i%2 == 0 { 253 | workers = 0 254 | } else { 255 | workers = runtime.NumCPU() 256 | } 257 | _, err := From(func(source chan<- interface{}) { 258 | for i := 1; i < 5; i++ { 259 | source <- i 260 | } 261 | }).Map(test.mapper, WithWorkers(workers)).Reduce( 262 | func(pipe <-chan interface{}) (interface{}, error) { 263 | for item := range pipe { 264 | result += item.(int) 265 | } 266 | return result, nil 267 | }) 268 | if err != nil { 269 | return 270 | } 271 | 272 | assert.Equal(t, test.expect, result) 273 | }) 274 | } 275 | } 276 | 277 | func TestMerge(t *testing.T) { 278 | Just(1, 2, 3, 4).Merge().ForEach(func(item interface{}) { 279 | assert.ElementsMatch(t, []interface{}{1, 2, 3, 4}, item.([]interface{})) 280 | }) 281 | } 282 | 283 | func TestParallelJust(t *testing.T) { 284 | var count int32 285 | Just(1, 2, 3).Parallel(func(item interface{}) { 286 | time.Sleep(time.Millisecond * 100) 287 | atomic.AddInt32(&count, int32(item.(int))) 288 | }, UnlimitedWorkers()) 289 | assert.Equal(t, int32(6), count) 290 | } 291 | 292 | func TestReverse(t *testing.T) { 293 | Just(1, 2, 3, 4).Reverse().Merge().ForEach(func(item interface{}) { 294 | assert.ElementsMatch(t, []interface{}{4, 3, 2, 1}, item.([]interface{})) 295 | }) 296 | } 297 | 298 | func TestSort(t *testing.T) { 299 | var prev int 300 | Just(5, 3, 7, 1, 9, 6, 4, 8, 2).Sort(func(a, b interface{}) bool { 301 | return a.(int) < b.(int) 302 | }).ForEach(func(item interface{}) { 303 | next := item.(int) 304 | assert.True(t, prev < next) 305 | prev = next 306 | }) 307 | } 308 | 309 | func TestSplit(t *testing.T) { 310 | assert.Panics(t, func() { 311 | Just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).Split(0).Done() 312 | }) 313 | var chunks [][]interface{} 314 | Just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).Split(4).ForEach(func(item interface{}) { 315 | chunk := item.([]interface{}) 316 | chunks = append(chunks, chunk) 317 | }) 318 | assert.EqualValues(t, [][]interface{}{ 319 | {1, 2, 3, 4}, 320 | {5, 6, 7, 8}, 321 | {9, 10}, 322 | }, chunks) 323 | } 324 | 325 | func TestTail(t *testing.T) { 326 | var result int 327 | _, err := Just(1, 2, 3, 4).Tail(2).Reduce(func(pipe <-chan interface{}) (interface{}, error) { 328 | for item := range pipe { 329 | result += item.(int) 330 | } 331 | return result, nil 332 | }) 333 | if err != nil { 334 | return 335 | } 336 | assert.Equal(t, 7, result) 337 | } 338 | 339 | func TestTailZero(t *testing.T) { 340 | assert.Panics(t, func() { 341 | _, err := Just(1, 2, 3, 4).Tail(0).Reduce(func(pipe <-chan interface{}) (interface{}, error) { 342 | return nil, nil 343 | }) 344 | if err != nil { 345 | return 346 | } 347 | }) 348 | } 349 | 350 | func TestWalk(t *testing.T) { 351 | var result int 352 | Just(1, 2, 3, 4, 5).Walk(func(item interface{}, pipe chan<- interface{}) { 353 | if item.(int)%2 != 0 { 354 | pipe <- item 355 | } 356 | }, UnlimitedWorkers()).ForEach(func(item interface{}) { 357 | result += item.(int) 358 | }) 359 | assert.Equal(t, 9, result) 360 | } 361 | 362 | func BenchmarkMapReduce(b *testing.B) { 363 | b.ReportAllocs() 364 | 365 | mapper := func(v interface{}) interface{} { 366 | return v.(int64) * v.(int64) 367 | } 368 | reducer := func(input <-chan interface{}) (interface{}, error) { 369 | var result int64 370 | for v := range input { 371 | result += v.(int64) 372 | } 373 | return result, nil 374 | } 375 | 376 | for i := 0; i < b.N; i++ { 377 | _, err := From(func(input chan<- interface{}) { 378 | for j := 0; j < 2; j++ { 379 | input <- int64(j) 380 | } 381 | }).Map(mapper).Reduce(reducer) 382 | if err != nil { 383 | return 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /tools/fx/ring.go: -------------------------------------------------------------------------------- 1 | package fx 2 | 3 | import "sync" 4 | 5 | // A Ring can be used as fixed size ring. 6 | type Ring struct { 7 | elements []interface{} 8 | index int 9 | lock sync.Mutex 10 | } 11 | 12 | // NewRing returns a Ring object with the given size n. 13 | func NewRing(n int) *Ring { 14 | if n < 1 { 15 | panic("n should be greater than 0") 16 | } 17 | 18 | return &Ring{ 19 | elements: make([]interface{}, n), 20 | } 21 | } 22 | 23 | // Add adds v into r. 24 | func (r *Ring) Add(v interface{}) { 25 | r.lock.Lock() 26 | defer r.lock.Unlock() 27 | 28 | r.elements[r.index%len(r.elements)] = v 29 | r.index++ 30 | } 31 | 32 | // Take takes all items from r. 33 | func (r *Ring) Take() []interface{} { 34 | r.lock.Lock() 35 | defer r.lock.Unlock() 36 | 37 | var size int 38 | var start int 39 | if r.index > len(r.elements) { 40 | size = len(r.elements) 41 | start = r.index % len(r.elements) 42 | } else { 43 | size = r.index 44 | } 45 | 46 | elements := make([]interface{}, size) 47 | for i := 0; i < size; i++ { 48 | elements[i] = r.elements[(start+i)%len(r.elements)] 49 | } 50 | 51 | return elements 52 | } 53 | -------------------------------------------------------------------------------- /tools/helper.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | ) 7 | 8 | func GoSafe(fn func()) { 9 | go runSafe(fn) 10 | } 11 | 12 | func runSafe(fn func()) { 13 | defer func() { 14 | if err := recover(); err != nil { 15 | debug.PrintStack() 16 | fmt.Printf("[runSafe] err:%v\n", err) 17 | } 18 | }() 19 | fn() 20 | } 21 | -------------------------------------------------------------------------------- /tools/restry.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/go-resty/resty/v2" 8 | ) 9 | 10 | var ( 11 | Resty *resty.Client 12 | 13 | defaultClient = &http.Client{ 14 | Timeout: 20 * time.Second, 15 | Transport: &http.Transport{ 16 | DisableKeepAlives: true, 17 | MaxIdleConns: 15, 18 | IdleConnTimeout: 90 * time.Second, 19 | }, 20 | } 21 | ) 22 | 23 | func init() { 24 | Resty = resty.NewWithClient(defaultClient) 25 | } 26 | --------------------------------------------------------------------------------