├── .gitignore ├── LICENSE ├── README.md ├── bank_card ├── README.md ├── assets │ ├── assets.go │ ├── bank_card.db │ ├── bin.csv │ └── name.csv ├── bank_card.go ├── bank_card_test.go ├── database.go ├── database_test.go ├── go.mod ├── go.sum ├── models.go ├── validator.go └── validator_test.go ├── byteutil ├── util.go └── util_test.go ├── copierutil ├── converters.go ├── converters_test.go ├── go.mod └── go.sum ├── crypto ├── README.md ├── aes.go ├── aes_test.go ├── crypto.go └── crypto_test.go ├── cryptocurrency ├── cryptocurrency.go └── cryptocurrency_test.go ├── dateutil ├── dateutils.go └── dateutils_test.go ├── entgo ├── ent_client.go ├── go.mod ├── go.sum ├── mixin │ ├── autoincrement_id.go │ ├── creator_id.go │ ├── operator.go │ ├── remark.go │ ├── snowflake_id.go │ ├── string_id.go │ ├── switch_status.go │ ├── time.go │ ├── timestamp.go │ └── uuid_id.go ├── query │ ├── README.md │ ├── filter.go │ ├── filter_test.go │ ├── order.go │ ├── pagination.go │ ├── query.go │ ├── query_test.go │ ├── select.go │ └── select_test.go └── update │ ├── update.go │ └── update_test.go ├── fieldmaskutil ├── fieldmaskutil.go └── fieldmaskutil_test.go ├── geoip ├── README.md ├── geoip.go ├── geoip_test.go ├── geolite │ ├── README.md │ ├── assets │ │ ├── GeoLite2-City.mmdb │ │ └── assets.go │ ├── geolite.go │ └── geolite_test.go ├── go.mod ├── go.sum └── qqwry │ ├── README.md │ ├── assets │ ├── assets.go │ └── qqwry.dat │ ├── consts.go │ ├── qqwry.go │ ├── qqwry_test.go │ ├── utils.go │ └── utils_test.go ├── gitautotag.sh ├── go.mod ├── go.sum ├── gorm ├── go.mod ├── go.sum ├── gorm_client.go └── migrates.go ├── id ├── README.md ├── go.mod ├── go.sum ├── order_id.go ├── order_id_test.go ├── snowflake.go ├── sonyflake.go ├── uuid.go └── uuid_test.go ├── ioutil ├── file.go ├── path.go └── path_test.go ├── jwtutil ├── go.mod ├── go.sum ├── jwt.go └── jwt_test.go ├── maputils ├── maputils.go └── maputils_test.go ├── math ├── gaussian.go ├── gaussian_test.go ├── math.go └── math_test.go ├── pagination └── pagination.go ├── rand ├── rand.go ├── rand_test.go ├── randomizer.go ├── randomizer_test.go ├── seeder.go └── seeder_test.go ├── sliceutil ├── sliceutils.go └── sliceutils_test.go ├── slug ├── go.mod ├── go.sum ├── slug.go └── slug_test.go ├── stringcase ├── stringcase.go └── stringcase_test.go ├── stringutil ├── cryptorandomstringutils.go ├── cryptorandomstringutils_test.go ├── randomstringutils.go ├── randomstringutils_test.go ├── replace.go └── replace_test.go ├── structutil ├── structutils.go └── structutils_test.go ├── tag.bat ├── timeutil ├── consts.go ├── diff.go ├── diff_test.go ├── format.go ├── format_test.go ├── range.go ├── range_test.go ├── trans.go └── trans_test.go ├── trans ├── to.go ├── to_test.go ├── trans.go └── trans_test.go ├── translator ├── README.md ├── go.mod ├── go.sum ├── google │ ├── options.go │ ├── translator.go │ ├── translator_test.go │ ├── utils.go │ ├── v1.go │ ├── v2.go │ └── v3.go ├── translator.go └── translator_test.go └── upgrade.bat /.gitignore: -------------------------------------------------------------------------------- 1 | # Reference https://github.com/github/gitignore/blob/master/Go.gitignore 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 19 | *.o 20 | *.a 21 | 22 | # OS General 23 | Thumbs.db 24 | .DS_Store 25 | 26 | # project 27 | *.cert 28 | *.key 29 | *.log 30 | bin/ 31 | 32 | # Develop tools 33 | .vscode/ 34 | .idea/ 35 | *.swp 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bobo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-utils 2 | -------------------------------------------------------------------------------- /bank_card/README.md: -------------------------------------------------------------------------------- 1 | # 银行卡BIN查询银行 2 | 3 | ## 参考资料 4 | 5 | - [China UnionPay Bank Card BIN Checker](https://github.com/hexindai/bcbc) 6 | - [CommonUtilLibrary](https://github.com/AbrahamCaiJin/CommonUtilLibrary/blob/master/CommonUtil/src/main/java/com/jingewenku/abrahamcaijin/commonutil/BankCheck.java) 7 | - [Luhn algorithm - wikipedia](https://en.wikipedia.org/wiki/Luhn_algorithm) 8 | - [Luhn algorithm - geeksforgeeks](https://www.geeksforgeeks.org/luhn-algorithm/) 9 | - [Luhn’s algorithm to validate credit / debit card Numbers](https://medium.com/@akshaymohite/luhns-algorithm-to-validate-credit-debit-card-numbers-1952e6c7a9d0) 10 | - [干货丨银行卡号编码规则及其应用](https://www.woshipm.com/pd/371041.html) 11 | - [bankInfo](https://github.com/giraffe-lib/bankInfo/blob/main/src/map.js) 12 | - [BankCards](https://github.com/geekgao/BankCards/blob/master/bankcode.py) 13 | - [bcbc](https://github.com/hexindai/bcbc) 14 | - [banks-db](https://github.com/ramoona/banks-db) 15 | - [SwiftCodes](https://github.com/PeterNotenboom/SwiftCodes) 16 | - [BanksDataWorldWide](https://github.com/abdalrhman-alajlouni/BanksDataWorldWide) 17 | - [bankcard](https://github.com/caijf/bankcard) 18 | -------------------------------------------------------------------------------- /bank_card/assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import _ "embed" 4 | 5 | //go:embed bank_card.db 6 | var BankCardDatabase []byte 7 | -------------------------------------------------------------------------------- /bank_card/assets/bank_card.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tx7do/go-utils/b2efe51e40a5acc2e89522ac34975439a3ffda33/bank_card/assets/bank_card.db -------------------------------------------------------------------------------- /bank_card/assets/name.csv: -------------------------------------------------------------------------------- 1 | ABC,中国农业银行 2 | AEON,AEON银行 3 | ARCU,安徽省农村信用社 4 | ASCB,鞍山银行 5 | AYCB,安阳银行 6 | 7 | BANKWF,潍坊银行 8 | BCDMMA,澳门商业银行 9 | BDCBANK,保定银行 10 | BEAHK,东亚银行有限公司 11 | BGB,广西北部湾银行 12 | BHB,河北银行 13 | BJBANK,北京银行 14 | BJRCB,北京农村商业银行 15 | BNU,大西洋银行 16 | BOC,中国银行 17 | BOCD,承德银行 18 | BOCFCB,中银富登村镇银行 19 | BOCY,朝阳银行 20 | BOCZ,沧州银行 21 | BOD,东莞银行 22 | BODD,丹东银行 23 | BOHAIB,渤海银行 24 | BOHN,海南省农村信用社 25 | BOJZ,锦州银行 26 | BOL,洛阳银行 27 | BOP,平顶山银行 28 | BOQH,青海银行 29 | BOQZ,泉州银行 30 | BOSH,新韩银行(中国) 31 | BOSZ,苏州银行 32 | BOYK,营口银行 33 | BOZK,周口银行 34 | BSB,包商银行 35 | BZMD,驻马店银行 36 | 37 | CAB,宝鸡商行 38 | CABANK,长安银行 39 | CBBQS,城市商业银行资金清算中心 40 | CBKF,开封市商业银行 41 | CCB,中国建设银行 42 | CCQTGB,重庆三峡银行 43 | CDB,国家开发银行 44 | CDCB,成都银行 45 | CDRCB,成都农商银行 46 | CEB,中国光大银行 47 | CEP,中银通商务支付有限公司 48 | CGNB,南充市商业银行 49 | CHB,创兴银行有限公司 50 | CIB,兴业银行 51 | CITI,花旗银行(中国) 52 | CITIC,中信银行 53 | CJCCB,苏州长江商业银行 54 | CMB,招商银行 55 | CMBC,中国民生银行 56 | COMM,交通银行 57 | COMMHK,交通银行香港分行 58 | CQBANK,重庆银行 59 | CRCBANK,重庆农村商业银行 60 | CSC,CSC借记卡 61 | CSCB,长沙银行 62 | CSRCB,常熟农商银行 63 | CYBC,集友银行 64 | CYS,中国银盛 65 | CZBANK,浙商银行 66 | CZCB,浙江稠州商业银行 67 | CZCCB,长治市商业银行 68 | CZRCB,常州农村信用联社 69 | 70 | DAQINGB,龙江银行 71 | DBS,星展银行 72 | DBSCN,星展银行 73 | DLB,大连银行 74 | DRCBCL,东莞农村商业银行 75 | DSB,大新银行 76 | DSBCN,大新银行(中国) 77 | DSBHK,大新银行(香港) 78 | DTCCB,大同银行 79 | DYCB,德阳商业银行 80 | DYCCB,东营市商业银行 81 | DZBANK,德州银行 82 | DZCBANK,达州银行 83 | 84 | EGBANK,恒丰银行 85 | ERB,龙岗鼎业村镇银行 86 | ESRCB,恩施农村商业银行 87 | 88 | FBBANK,富邦华一银行 89 | FDB,富滇银行 90 | FJHXBC,福建海峡银行 91 | FJNX,福建省农村信用社联合社 92 | FMBANK,吉林丰满惠民村镇银行 93 | FSCB,抚顺银行 94 | FSCCB,抚顺银行股份有限公司 95 | FXCB,阜新银行 96 | 97 | GCB,广州银行 98 | GDB,广东发展银行 99 | GDRCC,广东省农村信用社联合社 100 | GHB,广东华兴银行 101 | GLBANK,桂林银行 102 | GRCB,广州农商银行 103 | GSBANK,甘肃银行 104 | GSRCU,甘肃省农村信用社 105 | GXHMVB,长春高新惠民村镇银行 106 | GXRCU,广西省农村信用社 107 | GYCB,贵阳市商业银行 108 | GZB,赣州银行 109 | GZRCU,贵州省农村信用社 110 | 111 | H3CB,内蒙古银行 112 | HANABANK,韩亚银行 113 | HBC,湖北银行 114 | HBHSBANK,湖北银行黄石分行 115 | HBRCU,河北省农村信用社 116 | HBYCBANK,湖北银行宜昌分行 117 | HDBANK,邯郸银行 118 | HDHMCB,惠东惠民村镇银行 119 | HKB,汉口银行 120 | HKBEA,东亚银行 121 | HLDB,葫芦岛市商业银行 122 | HLDCCB,葫芦岛市商业银行 123 | HLJRCU,黑龙江省农村信用社 124 | HNRCC,湖南省农村信用社 125 | HNRCU,河南省农村信用社 126 | HRBANK,哈尔滨银行 127 | HRXJB,华融湘江银行 128 | HSB,恒生银行 129 | HSBANK,徽商银行 130 | HSBC,汇丰银行(中国) 131 | HSBK,衡水银行 132 | HURCB,湖北省农村信用社 133 | HXBANK,华夏银行 134 | HZCB,杭州银行 135 | HZCCB,湖州市商业银行 136 | 137 | ICBC,中国工商银行 138 | 139 | JHBANK,金华银行 140 | JINCHB,晋城银行 141 | JJBANK,九江银行 142 | JLBANK,吉林银行 143 | JLRCU,吉林农信 144 | JNBANK,济宁银行 145 | JRCB,江苏江阴农村商业银行 146 | JSB,晋商银行 147 | JSBANK,江苏银行 148 | JSRCU,江苏省农村信用联合社 149 | JXBANK,嘉兴银行 150 | JXRCU,江西省农村信用社 151 | JZBANK,晋中市商业银行 152 | JZCBANK,焦作市商业银行 153 | 154 | KLB,昆仑银行 155 | KMRCU,昆明农村信用联合社 156 | KORLABANK,库尔勒市商业银行 157 | KSRB,昆山农村商业银行 158 | KSRCB,昆山农信社 159 | 160 | LANGFB,廊坊银行 161 | LNRCC,辽宁省农村信用社 162 | LSB,临商银行 163 | LSBANK,莱商银行 164 | LSBC,临商银行 165 | LSCCB,乐山市商业银行 166 | LSZRCB,凉山州商业银行 167 | LUZBANK,泸州市商业银行 168 | LYBANK,洛阳银行 169 | LYCB,辽阳市商业银行 170 | LZCCB,柳州银行 171 | LZYH,兰州银行 172 | 173 | MTBANK,浙江民泰商业银行 174 | MYBANK,绵阳市商业银行 175 | 176 | NBBANK,宁波银行 177 | NBYZ,鄞州银行 178 | NCB,江西银行 179 | NCBANK,宁波通商银行 180 | NDHB,宁波东海银行 181 | NHB,南海农商银行 182 | NHQS,农信银清算中心 183 | NJCB,南京银行 184 | NMGNXS,内蒙古农村信用社 185 | NTCCB,南通商业银行 186 | NXBANK,宁夏银行 187 | NXRCU,宁夏黄河农村商业银行 188 | NYBANK,广东南粤银行 189 | NYCB,南阳村镇银行 190 | NYNB,广东南粤银行 191 | 192 | OCBC,华侨银行(中国) 193 | ORBANK,鄂尔多斯银行 194 | 195 | PLCCB,平凉市商业银行 196 | PSBC,中国邮政储蓄银行 197 | PZBANK,盘锦市商业银行 198 | PZHCCB,攀枝花市商业银行 199 | 200 | QDCCB,青岛银行 201 | QDRCB,青岛农村商业银行 202 | QHDBANK,秦皇岛银行 203 | QHDCCB,秦皇岛市商业银行 204 | QHRC,青海农村信用社 205 | QJCCB,曲靖市商业银行 206 | QLBANK,齐鲁银行 207 | QLCZYH,齐鲁村镇银行 208 | 209 | RBOZ,珠海华润银行 210 | RXCB,榆次融信村镇银行 211 | RZB,日照银行 212 | RZCCB,日照银行 213 | 214 | SCB,渣打银行 215 | SCBHK,渣打银行(香港) 216 | SCCB,三门峡银行 217 | SCRCU,四川省农村信用社 218 | SDEB,顺德农商银行 219 | SDRCU,山东省农村信用社 220 | SHBANK,上海银行 221 | SHCBHK,上海商业银行(香港) 222 | SHRCB,上海农村商业银行 223 | SJBANK,盛京银行 224 | SLH,湖南农村信用社联合社 225 | SLRCB,双流诚民村镇银行 226 | SNCCB,遂宁市商业银行 227 | SPABANK,平安银行 228 | SPDB,上海浦东发展银行 229 | SRBANK,上饶银行 230 | SRCB,深圳农村商业银行 231 | SXCB,绍兴银行 232 | SXRCCU,陕西信合 233 | SXRCU,山西省农村信用社 234 | SZSBK,石嘴山银行 235 | 236 | TACCB,泰安市商业银行 237 | TCCB,天津银行 238 | TCRCB,江苏太仓农村商业银行 239 | TFB,大丰银行有限公司 240 | TJBHB,天津滨海农村商业银行 241 | TLBANK,铁岭银行 242 | TRCB,天津农商银行 243 | TSBANK,唐山银行 244 | TZCB,台州银行 245 | 246 | URB,联合村镇银行 247 | URMQCCB,乌鲁木齐市商业银行 248 | 249 | WHB,永亨银行 250 | WHBANK,乌海银行 251 | WHBMA,澳门永亨银行 252 | WHCCB,威海市商业银行 253 | WHRCB,武汉农村商业银行 254 | WJRCB,吴江农商银行 255 | WLB,永隆银行 256 | WLBHK,永隆银行 257 | WOORI,友利银行(中国) 258 | WRCB,无锡农村商业银行 259 | WXCCB,无锡市商业银行 260 | WZCB,温州银行 261 | 262 | XABANK,西安银行 263 | XCYH,许昌银行 264 | XFRCB,咸丰常农商村镇银行 265 | XHCZYH,新华村镇银行 266 | XHRCB,新会农商银行 267 | XJRCU,新疆农村信用社 268 | XLBANK,中山小榄村镇银行 269 | XMBANK,厦门银行 270 | XMCCB,厦门市商业银行 271 | XTB,邢台银行 272 | XXBANK,新乡银行 273 | XYBANK,信阳银行 274 | 275 | YACCB,雅安市商业银行 276 | YBCCB,宜宾市商业银行 277 | YCCB,宜昌市商业银行 278 | YCCCB,盐城商行 279 | YDNSCZYH,尧都农商村镇银行 280 | YDRCB,尧都农商行 281 | YKYHCCB,营口沿海银行 282 | YNRCC,云南省农村信用社 283 | YQCCB,阳泉银行 284 | YTBANK,烟台银行 285 | YXCCB,玉溪市商业银行 286 | YZBANK,银座银行 287 | 288 | ZBCB,齐商银行 289 | ZGCCB,自贡市商业银行 290 | ZHCCB,珠海华润银行股份有限公司 291 | ZJCCB,镇江市商业银行 292 | ZJKCCB,张家口市商业银行 293 | ZJNX,浙江省农村信用社联合社 294 | ZJTLCB,浙江泰隆商业银行 295 | ZRCBANK,张家港农村商业银行 296 | ZYB,中原银行 297 | ZYCBANK,遵义市商业银行 298 | ZZBANK,郑州银行 299 | ZZCCB,郑州商业银行 -------------------------------------------------------------------------------- /bank_card/bank_card_test.go: -------------------------------------------------------------------------------- 1 | package bank_card 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetNameOfBank(t *testing.T) { 11 | var validBankCards = []string{ 12 | "6226095711989751", 13 | "6228480402564890018", 14 | "6228480402637874213", 15 | "6228481552887309119", 16 | "6228480801416266113", 17 | "6228481698729890079", 18 | "621661280000447287", 19 | "6222081106004039591", 20 | "6201140000000000000", 21 | } 22 | 23 | for _, w := range validBankCards { 24 | t.Run("get bank card of name: "+w, func(t *testing.T) { 25 | name := GetNameOfBank(w) 26 | fmt.Println(w, name) 27 | assert.True(t, len(name) > 0) 28 | }) 29 | } 30 | } 31 | 32 | func TestQueryBankByCardNumber(t *testing.T) { 33 | var validBankCards = []string{ 34 | "6226095711989751", 35 | "6228480402564890018", 36 | "6228480402637874213", 37 | "6228481552887309119", 38 | "6228480801416266113", 39 | "6228481698729890079", 40 | "621661280000447287", 41 | "6222081106004039591", 42 | "6201140000000000000", 43 | } 44 | 45 | for _, w := range validBankCards { 46 | t.Run("get bank card of name: "+w, func(t *testing.T) { 47 | bankCard := QueryBankByCardNumber(w) 48 | fmt.Println(w, bankCard) 49 | assert.NotNil(t, bankCard) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bank_card/database.go: -------------------------------------------------------------------------------- 1 | package bank_card 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "log" 9 | 10 | "github.com/mattn/go-sqlite3" 11 | _ "github.com/mattn/go-sqlite3" 12 | 13 | "github.com/tx7do/go-utils/bank_card/assets" 14 | ) 15 | 16 | type Database struct { 17 | db *sql.DB 18 | } 19 | 20 | func NewDatabase(openFile bool) *Database { 21 | db := &Database{} 22 | 23 | if openFile { 24 | db.openFromFile() 25 | } else { 26 | _ = db.openFromEmbed() 27 | } 28 | 29 | return db 30 | } 31 | 32 | // openFromFile 从文件打开数据库 33 | func (d *Database) openFromFile() { 34 | db, err := sql.Open("sqlite3", "assets/bank_card.db") 35 | if err != nil { 36 | panic("failed to connect database") 37 | } 38 | 39 | d.db = db 40 | 41 | d.initTables() 42 | } 43 | 44 | // openFromEmbed 从内嵌文件打开数据库 45 | func (d *Database) openFromEmbed() error { 46 | db, err := sql.Open("sqlite3", "file::memory:?mode=ro&cache=shared") 47 | if err != nil { 48 | panic("failed to connect database") 49 | return err 50 | } 51 | 52 | conn, err := db.Conn(context.Background()) 53 | if err != nil { 54 | log.Fatal(err) 55 | return err 56 | } 57 | defer conn.Close() 58 | 59 | if err = conn.Raw(func(raw interface{}) error { 60 | return raw.(*sqlite3.SQLiteConn).Deserialize(assets.BankCardDatabase, "") 61 | }); err != nil { 62 | log.Fatal(err) 63 | return err 64 | } 65 | conn.Close() 66 | 67 | d.db = db 68 | 69 | return nil 70 | } 71 | 72 | func (d *Database) Close() { 73 | if d.db != nil { 74 | _ = d.db.Close() 75 | } 76 | } 77 | 78 | // initTables 创建表 79 | func (d *Database) initTables() { 80 | strSql := ` 81 | CREATE TABLE IF NOT EXISTS bank_cards 82 | ( 83 | bin INTEGER PRIMARY KEY, 84 | bank_code TEXT, 85 | card_name TEXT, 86 | card_type TEXT, 87 | card_length INTEGER 88 | ); 89 | 90 | CREATE TABLE IF NOT EXISTS banks 91 | ( 92 | id INTEGER PRIMARY KEY autoincrement, 93 | bank_code TEXT, 94 | bank_name TEXT 95 | ); 96 | ` 97 | _, err := d.db.Exec(strSql) 98 | if err != nil { 99 | log.Println("initial database table failed:", err) 100 | } 101 | } 102 | 103 | func (d *Database) insertDataToBankTable(data *Bank) { 104 | strSql := fmt.Sprintf("INSERT INTO banks (bank_code, bank_name) VALUES ('%s', '%s');", data.BankCode, data.BankName) 105 | _, err := d.db.Exec(strSql) 106 | if err != nil { 107 | log.Println(err) 108 | } 109 | } 110 | 111 | func (d *Database) insertDataToBankCardTable(data *BankCard) { 112 | strSql := fmt.Sprintf("INSERT INTO bank_cards (bin, bank_code, card_name, card_type, card_length) VALUES (%d, '%s', '%s', '%s', %d);", 113 | data.BIN, data.BankCode, data.CardName, data.CardType, data.CardLength) 114 | _, err := d.db.Exec(strSql) 115 | if err != nil { 116 | log.Println(err) 117 | } 118 | } 119 | 120 | func (d *Database) UpdateBankCardTableCardName(bin uint32, cardName string) { 121 | strSql := fmt.Sprintf("UPDATE bank_cards SET card_name = '%s' WHERE bin = '%d';", cardName, bin) 122 | _, err := d.db.Exec(strSql) 123 | if err != nil { 124 | log.Println(err) 125 | } 126 | } 127 | 128 | // queryBank 查询银行信息 129 | func (d *Database) queryBank(bankCode string) *Bank { 130 | strSql := fmt.Sprintf("SELECT id, bank_code, bank_name FROM banks WHERE bank_code = '%s' LIMIT 1;", bankCode) 131 | row := d.db.QueryRow(strSql) 132 | if row == nil { 133 | return nil 134 | } 135 | 136 | var bank Bank 137 | if err := row.Scan(&bank.Id, &bank.BankCode, &bank.BankName); err != nil { 138 | if errors.Is(err, sql.ErrNoRows) { 139 | return nil 140 | } 141 | log.Fatal("scan bank failed:", err) 142 | return nil 143 | } 144 | 145 | //fmt.Printf("bank = %v\n", bank) 146 | 147 | return &bank 148 | } 149 | 150 | // queryBankCard 查询银行卡信息 151 | func (d *Database) queryBankCard(bin uint32) *BankCard { 152 | strSql := fmt.Sprintf("SELECT bin, bank_code, card_name, card_type, card_length FROM bank_cards WHERE bin = '%d' LIMIT 1;", bin) 153 | row := d.db.QueryRow(strSql) 154 | if row == nil { 155 | return nil 156 | } 157 | 158 | var bankCard BankCard 159 | if err := row.Scan(&bankCard.BIN, &bankCard.BankCode, &bankCard.CardName, &bankCard.CardType, &bankCard.CardLength); err != nil { 160 | if errors.Is(err, sql.ErrNoRows) { 161 | return nil 162 | } 163 | log.Fatal("scan bank card failed:", err) 164 | return nil 165 | } 166 | 167 | bank := d.queryBank(bankCard.BankCode) 168 | if bank != nil { 169 | bankCard.BankName = bank.BankName 170 | } 171 | 172 | //fmt.Printf("bankCard = %v\n", bankCard) 173 | 174 | return &bankCard 175 | } 176 | -------------------------------------------------------------------------------- /bank_card/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tx7do/go-utils/bank_card 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/mattn/go-sqlite3 v1.14.28 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | 16 | replace github.com/tx7do/go-utils => ../ 17 | -------------------------------------------------------------------------------- /bank_card/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= 4 | github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 8 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /bank_card/models.go: -------------------------------------------------------------------------------- 1 | package bank_card 2 | 3 | type CardType string 4 | 5 | const ( 6 | CardTypeDC CardType = "DC" // 储蓄卡 7 | CardTypeCC CardType = "CC" // 信用卡 8 | CardTypeSCC CardType = "SCC" // 准贷记卡 9 | CardTypePC CardType = "PC" // 预付费卡 10 | ) 11 | 12 | // Bank 银行信息 13 | type Bank struct { 14 | Id uint32 `gorm:"primarykey,column:id"` // ID 15 | BankCode string `gorm:"uniqueIndex,column:bank_code"` // 银行简称 16 | BankName string `gorm:"column:bank_name"` // 银行名称 17 | } 18 | 19 | // BankCard 银行卡信息 20 | type BankCard struct { 21 | BIN uint32 `gorm:"primarykey,column:bin"` // 银行识别码 22 | BankCode string `gorm:"column:bank_code"` // 银行代码 23 | BankName string // 银行名称 24 | CardType string `gorm:"column:card_type"` // 银行卡类型 25 | CardName string `gorm:"column:card_name"` // 银行卡名称 26 | CardLength uint32 `gorm:"column:card_length"` // 银行卡号长度 27 | } 28 | 29 | // CardTypeName 将卡类型转为类型名 30 | func (b *BankCard) CardTypeName() string { 31 | switch CardType(b.CardType) { 32 | case CardTypeDC: 33 | return "储蓄卡" 34 | case CardTypeCC: 35 | return "信用卡" 36 | case CardTypeSCC: 37 | return "准贷记卡" 38 | case CardTypePC: 39 | return "预付费卡" 40 | } 41 | return "" 42 | } 43 | -------------------------------------------------------------------------------- /bank_card/validator.go: -------------------------------------------------------------------------------- 1 | package bank_card 2 | 3 | // IsValidLuhn 使用Luhn算法校验银行卡号码 4 | // @see https://en.wikipedia.org/wiki/Luhn_algorithm 5 | // @see https://www.geeksforgeeks.org/luhn-algorithm/ 6 | // @see https://medium.com/@akshaymohite/luhns-algorithm-to-validate-credit-debit-card-numbers-1952e6c7a9d0 7 | // @see https://www.woshipm.com/pd/371041.html 8 | func IsValidLuhn(cardNo string) bool { 9 | length := len(cardNo) 10 | if length == 0 { 11 | return false 12 | } 13 | 14 | if !isNumberString(cardNo) { 15 | return false 16 | } 17 | 18 | sum := 0 19 | second := false 20 | for i := length - 1; i >= 0; i-- { 21 | d := cardNo[i] - '0' 22 | 23 | if second == true { 24 | d = d * 2 25 | } 26 | 27 | sum += int(d) / 10 28 | sum += int(d) % 10 29 | 30 | second = !second 31 | } 32 | 33 | return sum%10 == 0 34 | } 35 | 36 | // IsValidBankCardNo 是否合法的银行卡号 37 | func IsValidBankCardNo(cardNo string) bool { 38 | length := len(cardNo) 39 | if length < 12 || length > 19 { 40 | return false 41 | } 42 | return IsValidLuhn(cardNo) 43 | } 44 | 45 | // isNumberString 验证字符是数字 46 | func isNumberString(s string) bool { 47 | length := len(s) 48 | for i := 0; i < length; i++ { 49 | if s[i] < '0' || s[i] > '9' { 50 | return false 51 | } 52 | } 53 | return true 54 | } 55 | -------------------------------------------------------------------------------- /bank_card/validator_test.go: -------------------------------------------------------------------------------- 1 | package bank_card 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestIsValidLuhn(t *testing.T) { 9 | var validBankCards = []string{ 10 | "79927398713", 11 | "6226095711989751", 12 | "49927398716", 13 | "1234567812345670", 14 | "356938035643809", 15 | "1111222233334444", 16 | "374652346956782346957823694857692364857368475368", 17 | "6228480402564890018", 18 | "6228480402637874213", 19 | "6228481552887309119", 20 | "6228480801416266113", 21 | "6228481698729890079", 22 | "6222081106004039591", 23 | } 24 | 25 | var invalidBankCards = []string{ 26 | "49927398717", 27 | "1234567812345678", 28 | "314143525252", 29 | "534618613411236", 30 | "1234567879855431", 31 | "374652346956782346957823694857692364857387456834", 32 | "79927398710", 33 | "79927398711", 34 | "79927398711", 35 | "79927398712", 36 | "79927398714", 37 | "79927398715", 38 | "79927398716", 39 | "79927398717", 40 | "79927398718", 41 | "79927398719", 42 | "621661280000447287", 43 | } 44 | 45 | for _, w := range validBankCards { 46 | t.Run("valid bank card: "+w, func(t *testing.T) { 47 | assert.True(t, IsValidLuhn(w)) 48 | }) 49 | } 50 | 51 | for _, w := range invalidBankCards { 52 | t.Run("invalid bank card: "+w, func(t *testing.T) { 53 | assert.False(t, IsValidLuhn(w)) 54 | }) 55 | } 56 | } 57 | 58 | func TestIsValidBankCardNo(t *testing.T) { 59 | var validBankCards = []string{ 60 | "6226095711989751", 61 | "6228480402564890018", 62 | "6228480402637874213", 63 | "6228481552887309119", 64 | "6228480801416266113", 65 | "6228481698729890079", 66 | "6222081106004039591", 67 | "1234567812345670", 68 | "356938035643809", 69 | "1111222233334444", 70 | } 71 | 72 | var invalidBankCards = []string{ 73 | "49927398717", 74 | "1234567812345678", 75 | "314143525252", 76 | "534618613411236", 77 | "1234567879855431", 78 | "374652346956782346957823694857692364857387456834", 79 | "79927398710", 80 | "79927398711", 81 | "79927398711", 82 | "79927398712", 83 | "79927398714", 84 | "79927398715", 85 | "79927398716", 86 | "79927398717", 87 | "79927398718", 88 | "79927398719", 89 | "621661280000447287", 90 | "79927398713", 91 | "49927398716", 92 | "374652346956782346957823694857692364857368475368", 93 | } 94 | 95 | for _, w := range validBankCards { 96 | t.Run("valid bank card: "+w, func(t *testing.T) { 97 | assert.True(t, IsValidBankCardNo(w)) 98 | }) 99 | } 100 | 101 | for _, w := range invalidBankCards { 102 | t.Run("invalid bank card: "+w, func(t *testing.T) { 103 | assert.False(t, IsValidBankCardNo(w)) 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /byteutil/util.go: -------------------------------------------------------------------------------- 1 | package byteutil 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // IntToBytes 将int转换为[]byte 9 | func IntToBytes(n int) []byte { 10 | data := int64(n) 11 | byteBuf := bytes.NewBuffer([]byte{}) 12 | _ = binary.Write(byteBuf, binary.BigEndian, data) 13 | return byteBuf.Bytes() 14 | } 15 | 16 | // BytesToInt 将[]byte转换为int 17 | func BytesToInt(bys []byte) int { 18 | byteBuf := bytes.NewBuffer(bys) 19 | var data int64 20 | _ = binary.Read(byteBuf, binary.BigEndian, &data) 21 | return int(data) 22 | } 23 | 24 | // ByteToLower lowers a byte 25 | func ByteToLower(b byte) byte { 26 | if b <= '\u007F' { 27 | if 'A' <= b && b <= 'Z' { 28 | b += 'a' - 'A' 29 | } 30 | return b 31 | } 32 | return b 33 | } 34 | 35 | // ByteToUpper upper a byte 36 | func ByteToUpper(b byte) byte { 37 | if b <= '\u007F' { 38 | if 'a' <= b && b <= 'z' { 39 | b -= 'a' - 'A' 40 | } 41 | return b 42 | } 43 | return b 44 | } 45 | -------------------------------------------------------------------------------- /byteutil/util_test.go: -------------------------------------------------------------------------------- 1 | package byteutil 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestIntToBytes(t *testing.T) { 9 | fmt.Println(IntToBytes(1)) 10 | fmt.Println(BytesToInt(IntToBytes(1))) 11 | } 12 | -------------------------------------------------------------------------------- /copierutil/converters.go: -------------------------------------------------------------------------------- 1 | package copierutil 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/copier" 7 | "google.golang.org/protobuf/types/known/timestamppb" 8 | 9 | "github.com/tx7do/go-utils/timeutil" 10 | "github.com/tx7do/go-utils/trans" 11 | ) 12 | 13 | var TimeToStringConverter = copier.TypeConverter{ 14 | SrcType: &time.Time{}, // 源类型 15 | DstType: trans.Ptr(""), // 目标类型 16 | Fn: func(src interface{}) (interface{}, error) { 17 | return timeutil.TimeToTimeString(src.(*time.Time)), nil 18 | }, 19 | } 20 | 21 | var StringToTimeConverter = copier.TypeConverter{ 22 | SrcType: trans.Ptr(""), 23 | DstType: &time.Time{}, 24 | Fn: func(src interface{}) (interface{}, error) { 25 | return timeutil.StringTimeToTime(src.(*string)), nil 26 | }, 27 | } 28 | 29 | var TimeToTimestamppbConverter = copier.TypeConverter{ 30 | SrcType: &time.Time{}, 31 | DstType: ×tamppb.Timestamp{}, 32 | Fn: func(src interface{}) (interface{}, error) { 33 | return timeutil.TimeToTimestamppb(src.(*time.Time)), nil 34 | }, 35 | } 36 | 37 | var TimestamppbToTimeConverter = copier.TypeConverter{ 38 | SrcType: ×tamppb.Timestamp{}, 39 | DstType: &time.Time{}, 40 | Fn: func(src interface{}) (interface{}, error) { 41 | return timeutil.TimestamppbToTime(src.(*timestamppb.Timestamp)), nil 42 | }, 43 | } 44 | 45 | func TimeToString(tm *time.Time) *string { 46 | return timeutil.TimeToString(tm, timeutil.ISO8601) 47 | } 48 | 49 | func NewTimeStringConverterPair() []copier.TypeConverter { 50 | srcType := &time.Time{} 51 | dstType := trans.Ptr("") 52 | 53 | fromFn := TimeToString 54 | toFn := timeutil.StringTimeToTime 55 | 56 | return NewGenericTypeConverterPair(srcType, dstType, fromFn, toFn) 57 | } 58 | 59 | func NewTimeTimestamppbConverterPair() []copier.TypeConverter { 60 | srcType := &time.Time{} 61 | dstType := ×tamppb.Timestamp{} 62 | 63 | fromFn := timeutil.TimeToTimestamppb 64 | toFn := timeutil.TimestamppbToTime 65 | 66 | return NewGenericTypeConverterPair(srcType, dstType, fromFn, toFn) 67 | } 68 | 69 | func NewTypeConverter(srcType, dstType interface{}, fn func(src interface{}) (interface{}, error)) copier.TypeConverter { 70 | return copier.TypeConverter{ 71 | SrcType: srcType, 72 | DstType: dstType, 73 | Fn: fn, 74 | } 75 | } 76 | 77 | func NewTypeConverterPair(srcType, dstType interface{}, fromFn, toFn func(src interface{}) (interface{}, error)) []copier.TypeConverter { 78 | return []copier.TypeConverter{ 79 | { 80 | SrcType: srcType, 81 | DstType: dstType, 82 | Fn: fromFn, 83 | }, 84 | { 85 | SrcType: dstType, 86 | DstType: srcType, 87 | Fn: toFn, 88 | }, 89 | } 90 | } 91 | 92 | func NewGenericTypeConverterPair[A interface{}, B interface{}](srcType A, dstType B, fromFn func(src A) B, toFn func(src B) A) []copier.TypeConverter { 93 | return []copier.TypeConverter{ 94 | { 95 | SrcType: srcType, 96 | DstType: dstType, 97 | Fn: func(src interface{}) (interface{}, error) { 98 | return fromFn(src.(A)), nil 99 | }, 100 | }, 101 | { 102 | SrcType: dstType, 103 | DstType: srcType, 104 | Fn: func(src interface{}) (interface{}, error) { 105 | return toFn(src.(B)), nil 106 | }, 107 | }, 108 | } 109 | } 110 | 111 | func NewErrorHandlingGenericTypeConverterPair[A interface{}, B interface{}](srcType A, dstType B, fromFn func(src A) (B, error), toFn func(src B) (A, error)) []copier.TypeConverter { 112 | return []copier.TypeConverter{ 113 | { 114 | SrcType: srcType, 115 | DstType: dstType, 116 | Fn: func(src interface{}) (interface{}, error) { 117 | return fromFn(src.(A)) 118 | }, 119 | }, 120 | { 121 | SrcType: dstType, 122 | DstType: srcType, 123 | Fn: func(src interface{}) (interface{}, error) { 124 | return toFn(src.(B)) 125 | }, 126 | }, 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /copierutil/converters_test.go: -------------------------------------------------------------------------------- 1 | package copierutil 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "google.golang.org/protobuf/types/known/timestamppb" 9 | 10 | "github.com/tx7do/go-utils/timeutil" 11 | "github.com/tx7do/go-utils/trans" 12 | ) 13 | 14 | func TestNewTypeConverter(t *testing.T) { 15 | srcType := &time.Time{} 16 | dstType := trans.Ptr("") 17 | fn := func(src interface{}) (interface{}, error) { 18 | return timeutil.TimeToTimeString(src.(*time.Time)), nil 19 | } 20 | 21 | converter := NewTypeConverter(srcType, dstType, fn) 22 | 23 | // 验证转换器的类型 24 | assert.IsType(t, srcType, converter.SrcType) 25 | assert.IsType(t, dstType, converter.DstType) 26 | 27 | // 验证转换器的功能 28 | result, err := converter.Fn(&time.Time{}) 29 | assert.NoError(t, err) 30 | assert.IsType(t, dstType, result) 31 | } 32 | 33 | func TestNewTypeConverterPair(t *testing.T) { 34 | srcType := &time.Time{} 35 | dstType := trans.Ptr("") 36 | fromFn := func(src interface{}) (interface{}, error) { 37 | return timeutil.TimeToTimeString(src.(*time.Time)), nil 38 | } 39 | toFn := func(src interface{}) (interface{}, error) { 40 | return timeutil.StringTimeToTime(src.(*string)), nil 41 | } 42 | 43 | converters := NewTypeConverterPair(srcType, dstType, fromFn, toFn) 44 | assert.Len(t, converters, 2, "expected 2 converters") 45 | 46 | // 验证第一个转换器 47 | assert.IsType(t, srcType, converters[0].SrcType) 48 | assert.IsType(t, dstType, converters[0].DstType) 49 | result, err := converters[0].Fn(&time.Time{}) 50 | assert.NoError(t, err) 51 | assert.IsType(t, dstType, result) 52 | 53 | // 验证第二个转换器 54 | assert.IsType(t, dstType, converters[1].SrcType) 55 | assert.IsType(t, srcType, converters[1].DstType) 56 | result, err = converters[1].Fn(trans.Ptr("")) 57 | assert.NoError(t, err) 58 | assert.IsType(t, srcType, result) 59 | } 60 | 61 | func TestNewGenericTypeConverterPair(t *testing.T) { 62 | srcType := &time.Time{} 63 | dstType := trans.Ptr("") 64 | fromFn := timeutil.TimeToTimeString 65 | toFn := timeutil.StringTimeToTime 66 | 67 | converters := NewGenericTypeConverterPair(srcType, dstType, fromFn, toFn) 68 | assert.Len(t, converters, 2, "expected 2 converters") 69 | 70 | // 验证第一个转换器 71 | assert.IsType(t, srcType, converters[0].SrcType) 72 | assert.IsType(t, dstType, converters[0].DstType) 73 | result, err := converters[0].Fn(&time.Time{}) 74 | assert.NoError(t, err) 75 | assert.IsType(t, dstType, result) 76 | 77 | // 验证第二个转换器 78 | assert.IsType(t, dstType, converters[1].SrcType) 79 | assert.IsType(t, srcType, converters[1].DstType) 80 | result, err = converters[1].Fn(trans.Ptr("")) 81 | assert.NoError(t, err) 82 | assert.IsType(t, srcType, result) 83 | } 84 | 85 | func TestNewErrorHandlingGenericTypeConverterPair(t *testing.T) { 86 | srcType := &time.Time{} 87 | dstType := trans.Ptr("") 88 | fromFn := func(src *time.Time) (*string, error) { 89 | return timeutil.TimeToTimeString(src), nil 90 | } 91 | toFn := func(src *string) (*time.Time, error) { 92 | return timeutil.StringTimeToTime(src), nil 93 | } 94 | 95 | converters := NewErrorHandlingGenericTypeConverterPair(srcType, dstType, fromFn, toFn) 96 | assert.Len(t, converters, 2, "expected 2 converters") 97 | 98 | // 验证第一个转换器 99 | assert.IsType(t, srcType, converters[0].SrcType) 100 | assert.IsType(t, dstType, converters[0].DstType) 101 | result, err := converters[0].Fn(&time.Time{}) 102 | assert.NoError(t, err) 103 | assert.IsType(t, dstType, result) 104 | 105 | // 验证第二个转换器 106 | assert.IsType(t, dstType, converters[1].SrcType) 107 | assert.IsType(t, srcType, converters[1].DstType) 108 | result, err = converters[1].Fn(trans.Ptr("")) 109 | assert.NoError(t, err) 110 | assert.IsType(t, srcType, result) 111 | } 112 | 113 | func TestNewTimeStringConverterPair(t *testing.T) { 114 | converters := NewTimeStringConverterPair() 115 | assert.Len(t, converters, 2, "expected 2 converters") 116 | 117 | // 验证第一个转换器 118 | srcType := &time.Time{} 119 | dstType := trans.Ptr("") 120 | assert.IsType(t, srcType, converters[0].SrcType) 121 | assert.IsType(t, dstType, converters[0].DstType) 122 | result, err := converters[0].Fn(&time.Time{}) 123 | assert.NoError(t, err) 124 | assert.IsType(t, dstType, result) 125 | 126 | // 验证第二个转换器 127 | assert.IsType(t, dstType, converters[1].SrcType) 128 | assert.IsType(t, srcType, converters[1].DstType) 129 | result, err = converters[1].Fn(trans.Ptr("")) 130 | assert.NoError(t, err) 131 | assert.IsType(t, srcType, result) 132 | } 133 | 134 | func TestNewTimeTimestamppbConverterPair(t *testing.T) { 135 | converters := NewTimeTimestamppbConverterPair() 136 | assert.Len(t, converters, 2, "expected 2 converters") 137 | 138 | // 验证第一个转换器 139 | srcType := &time.Time{} 140 | dstType := ×tamppb.Timestamp{} 141 | assert.IsType(t, srcType, converters[0].SrcType) 142 | assert.IsType(t, dstType, converters[0].DstType) 143 | result, err := converters[0].Fn(&time.Time{}) 144 | assert.NoError(t, err) 145 | assert.IsType(t, dstType, result) 146 | 147 | // 验证第二个转换器 148 | assert.IsType(t, dstType, converters[1].SrcType) 149 | assert.IsType(t, srcType, converters[1].DstType) 150 | result, err = converters[1].Fn(×tamppb.Timestamp{}) 151 | assert.NoError(t, err) 152 | assert.IsType(t, srcType, result) 153 | } 154 | -------------------------------------------------------------------------------- /copierutil/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tx7do/go-utils/copierutil 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/jinzhu/copier v0.4.0 9 | github.com/tx7do/go-utils v1.1.27 10 | ) 11 | 12 | require ( 13 | github.com/stretchr/testify v1.10.0 14 | google.golang.org/protobuf v1.36.6 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/kr/text v0.2.0 // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | 24 | replace github.com/tx7do/go-utils => ../ 25 | -------------------------------------------------------------------------------- /copierutil/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 5 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 6 | github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= 7 | github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 8 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 9 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 15 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 16 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 17 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 18 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 19 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 20 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 21 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 24 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 26 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | -------------------------------------------------------------------------------- /crypto/README.md: -------------------------------------------------------------------------------- 1 | # 加解密算法 2 | 3 | ## bcrypt 4 | 5 | bcrypt是一个由美国计算机科学家尼尔斯·普罗沃斯(Niels Provos)以及大卫·马齐耶(David Mazières)根据Blowfish加密算法所设计的密码散列函数,于1999年在USENIX中展示[1]。实现中bcrypt会使用一个加盐的流程以防御彩虹表攻击,同时bcrypt还是适应性函数,它可以借由增加迭代之次数来抵御日益增进的电脑运算能力透过暴力法破解。 6 | 7 | 由bcrypt加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符,并将在内部被转化为448位的密钥。然而,所提供的所有字符都具有十分重要的意义。密码越强大,数据就越安全。 8 | 9 | 除了对数据进行加密,默认情况下,bcrypt在删除数据之前将使用随机数据三次覆盖原始输入文件,以阻挠可能会获得计算机数据的人恢复数据的尝试。如果您不想使用此功能,可设置禁用此功能。 10 | 11 | 具体来说,bcrypt使用美国密码学家保罗·柯切尔的算法实现。随bcrypt一起发布的源代码对原始版本作了略微改动。 12 | 13 | bcrypt哈希由多个部分组成。这些部分用于确定创建哈希的设置,从而可以在不需要任何其他信息的情况下对其进行验证。 14 | 15 | ```text 16 | $2a$10$ygWrRwHCzg2GUpz0UK40kuWAGva121VkScpcdMNsDCih2U/bL2qYy 17 | ``` 18 | 19 | - $2a$:Prefix 表示使用bcrypt的算法版本。 20 | - 10$:Cost factor 表示加密的复杂度,值越大,计算时间越长。 21 | - ygWrRwHCzg2GUpz0UK40kuWAGva121VkScpcdMNsDCih2U/bL2qYy:Salt 和 Hash,前22个字符是盐值,后面的字符是哈希值。 22 | -------------------------------------------------------------------------------- /crypto/aes.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "crypto/aes" 8 | "crypto/cipher" 9 | "crypto/rand" 10 | ) 11 | 12 | // DefaultAESKey 默认AES密钥(16字节) 13 | var DefaultAESKey = []byte("f51d66a73d8a0927") 14 | 15 | // GenerateAESKey 生成AES密钥 16 | func GenerateAESKey(length int) ([]byte, error) { 17 | if length != 16 && length != 24 && length != 32 { 18 | return nil, fmt.Errorf("invalid key length: %d, must be 16, 24, or 32 bytes", length) 19 | } 20 | key := make([]byte, length) 21 | _, err := rand.Read(key) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return key, nil 26 | } 27 | 28 | // PKCS5Padding 填充明文 29 | func PKCS5Padding(plaintext []byte, blockSize int) []byte { 30 | padding := blockSize - len(plaintext)%blockSize 31 | padText := bytes.Repeat([]byte{byte(padding)}, padding) 32 | return append(plaintext, padText...) 33 | } 34 | 35 | // PKCS5UnPadding 去除填充数据 36 | func PKCS5UnPadding(origData []byte) []byte { 37 | length := len(origData) 38 | unpadding := int(origData[length-1]) 39 | return origData[:(length - unpadding)] 40 | } 41 | 42 | // AesEncrypt AES加密 43 | func AesEncrypt(plainText, key, iv []byte) ([]byte, error) { 44 | if plainText == nil { 45 | return nil, fmt.Errorf("plain text is nil") 46 | } 47 | if key == nil { 48 | return nil, fmt.Errorf("key is nil") 49 | } 50 | 51 | block, err := aes.NewCipher(key) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | // AES分组长度为128位,所以blockSize=16,单位字节 57 | blockSize := block.BlockSize() 58 | 59 | if iv == nil { 60 | // 初始向量的长度必须等于块block的长度16字节 61 | iv = key[:blockSize] 62 | } 63 | 64 | plainText = PKCS5Padding(plainText, blockSize) 65 | 66 | blockMode := cipher.NewCBCEncrypter(block, iv) 67 | cryptedText := make([]byte, len(plainText)) 68 | blockMode.CryptBlocks(cryptedText, plainText) 69 | return cryptedText, nil 70 | } 71 | 72 | // AesDecrypt AES解密 73 | func AesDecrypt(cryptedText, key, iv []byte) ([]byte, error) { 74 | if cryptedText == nil { 75 | return nil, fmt.Errorf("crypted text is nil") 76 | } 77 | if key == nil { 78 | return nil, fmt.Errorf("key is nil") 79 | } 80 | 81 | block, err := aes.NewCipher(key) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | //AES分组长度为128位,所以blockSize=16,单位字节 87 | blockSize := block.BlockSize() 88 | 89 | if iv == nil { 90 | // 初始向量的长度必须等于块block的长度16字节 91 | iv = key[:blockSize] 92 | } 93 | 94 | blockMode := cipher.NewCBCDecrypter(block, iv) 95 | 96 | plainText := make([]byte, len(cryptedText)) 97 | blockMode.CryptBlocks(plainText, cryptedText) 98 | plainText = PKCS5UnPadding(plainText) 99 | return plainText, nil 100 | } 101 | -------------------------------------------------------------------------------- /crypto/aes_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "encoding/base64" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestDecryptAES(t *testing.T) { 13 | //key的长度必须是16、24或者32字节,分别用于选择AES-128, AES-192, or AES-256 14 | aesKey, _ := GenerateAESKey(16) 15 | aesKey = DefaultAESKey 16 | 17 | plainText := []byte("cloud123456") 18 | encryptText, err := AesEncrypt(plainText, aesKey, nil) 19 | if err != nil { 20 | fmt.Println(err) 21 | return 22 | } 23 | 24 | pass64 := base64.StdEncoding.EncodeToString(encryptText) 25 | fmt.Printf("加密后:%v\n", pass64) 26 | 27 | bytesPass, err := base64.StdEncoding.DecodeString(pass64) 28 | if err != nil { 29 | fmt.Println(err) 30 | return 31 | } 32 | 33 | decryptText, err := AesDecrypt(bytesPass, aesKey, nil) 34 | if err != nil { 35 | fmt.Println(err) 36 | return 37 | } 38 | fmt.Printf("解密后:%s\n", decryptText) 39 | assert.Equal(t, plainText, decryptText) 40 | } 41 | 42 | func TestGenerateAESKey_ValidLengths(t *testing.T) { 43 | lengths := []int{16, 24, 32} 44 | for _, length := range lengths { 45 | key, err := GenerateAESKey(length) 46 | assert.NoError(t, err) 47 | assert.Equal(t, length, len(key)) 48 | t.Logf("%d : %x\n", length, string(key)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha256" 6 | 7 | "encoding/hex" 8 | 9 | "golang.org/x/crypto/bcrypt" 10 | ) 11 | 12 | // DefaultCost 最小值=4 最大值=31 默认值=10 13 | var DefaultCost = 10 14 | 15 | // HashPassword 加密密码 16 | func HashPassword(password string) (string, error) { 17 | // Prefix + Cost + Salt + Hashed Text 18 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), DefaultCost) 19 | return string(bytes), err 20 | } 21 | 22 | // HashPasswordWithSalt 对密码进行加盐哈希处理 23 | func HashPasswordWithSalt(password, salt string) (string, error) { 24 | // 将密码和盐组合 25 | combined := []byte(password + salt) 26 | 27 | // 计算哈希值 28 | hash := sha256.Sum256(combined) 29 | 30 | // 将哈希值转换为十六进制字符串 31 | return hex.EncodeToString(hash[:]), nil 32 | } 33 | 34 | // VerifyPassword 验证密码是否正确 35 | func VerifyPassword(password, hash string) bool { 36 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 37 | return err == nil 38 | } 39 | 40 | // VerifyPasswordWithSalt 验证密码是否正确 41 | func VerifyPasswordWithSalt(password, salt, hashedPassword string) bool { 42 | // 对输入的密码和盐进行哈希处理 43 | newHash, _ := HashPasswordWithSalt(password, salt) 44 | // 比较哈希值是否相同 45 | return newHash == hashedPassword 46 | } 47 | 48 | // GenerateSalt 生成指定长度的盐 49 | func GenerateSalt(length int) (string, error) { 50 | salt := make([]byte, length) 51 | _, err := rand.Read(salt) 52 | if err != nil { 53 | return "", err 54 | } 55 | return hex.EncodeToString(salt), nil 56 | } 57 | -------------------------------------------------------------------------------- /crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestHashPassword(t *testing.T) { 12 | text := "admin" 13 | hash, _ := HashPassword(text) 14 | fmt.Println(hash) 15 | } 16 | 17 | func TestVerifyPassword(t *testing.T) { 18 | text := "123456" 19 | 20 | // Prefix + Cost + Salt + Hashed Text 21 | hash3 := "$2a$10$ygWrRwHCzg2GUpz0UK40kuWAGva121VkScpcdMNsDCih2U/bL2qYy" 22 | bMatched := VerifyPassword(text, hash3) 23 | assert.True(t, bMatched) 24 | 25 | bMatched = VerifyPassword(text, hash3) 26 | assert.True(t, bMatched) 27 | } 28 | 29 | func TestVerifyPasswordWithSalt_CorrectPassword(t *testing.T) { 30 | password := "securePassword" 31 | salt, _ := GenerateSalt(16) 32 | hashedPassword, _ := HashPasswordWithSalt(password, salt) 33 | 34 | result := VerifyPasswordWithSalt(password, salt, hashedPassword) 35 | assert.True(t, result, "Password verification should succeed with correct password and salt") 36 | } 37 | 38 | func TestJwtToken(t *testing.T) { 39 | const bearerWord string = "Bearer" 40 | token := "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjowfQ.XgcKAAjHbA6o4sxxbEaMi05ingWvKdCNnyW9wowbJvs" 41 | auths := strings.SplitN(token, " ", 2) 42 | assert.Equal(t, len(auths), 2) 43 | assert.Equal(t, strings.EqualFold(auths[0], bearerWord), true, "JWT token is missing") 44 | } 45 | -------------------------------------------------------------------------------- /cryptocurrency/cryptocurrency.go: -------------------------------------------------------------------------------- 1 | package cryptocurrency 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var cryptoRegexMap = map[string]*regexp.Regexp{ 10 | "btc": regexp.MustCompile("^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$"), 11 | "btg": regexp.MustCompile("^([GA])[a-zA-HJ-NP-Z0-9]{24,34}$"), 12 | "dash": regexp.MustCompile("^([X7])[a-zA-Z0-9]{33}$"), 13 | "dgb": regexp.MustCompile("^(D)[a-zA-Z0-9]{24,33}$"), 14 | "eth": regexp.MustCompile("^(0x)[a-zA-Z0-9]{40}$"), 15 | "smart": regexp.MustCompile("^(S)[a-zA-Z0-9]{33}$"), 16 | "xrp": regexp.MustCompile("^(r)[a-zA-Z0-9]{33}$"), 17 | "zcr": regexp.MustCompile("^(Z)[a-zA-Z0-9]{33}$"), 18 | "zec": regexp.MustCompile("^(t)[a-zA-Z0-9]{34}$"), 19 | "xmr": regexp.MustCompile("/4[0-9AB][1-9A-HJ-NP-Za-km-z]{93}$"), 20 | "trc": regexp.MustCompile("T[A-Za-z1-9]{33}"), 21 | } 22 | 23 | // DetermineWalletType 判断钱包地址的类型 24 | func DetermineWalletType(wallet string) (string, error) { 25 | if strings.HasPrefix(wallet, "0x") { 26 | if len(wallet) != 42 { 27 | return "", errors.New("无效的ETH地址") 28 | } 29 | return "eth", nil 30 | } else if strings.HasPrefix(wallet, "T") { 31 | if len(wallet) != 34 { 32 | return "", errors.New("无效的TRC地址") 33 | } 34 | return "trc", nil 35 | } else { 36 | if len(wallet) != 34 { 37 | return "", errors.New("无效的OMINI地址") 38 | } 39 | return "omini", nil 40 | } 41 | } 42 | 43 | // IsValidBTCAddress 是否有效的BTC地址 44 | // 简称:OMNI 45 | // 使用的比特币地址的正确格式: 46 | // 1. BTC 地址是包含 26-35 个字母数字字符的标识符。 47 | // 2. BTC 地址以数字 1、3 或 bc1 开头。 48 | // 3. 它包含 0 到 9 范围内的数字。 49 | // 4. 它允许使用大写和小写字母字符。 50 | // 5. 有一点需要注意:没有使用大写字母 O、大写字母 I、小写字母 l 和数字 0,以避免视觉上的歧义。 51 | // 6. 它不应包含空格和其他特殊字符。 52 | func IsValidBTCAddress(address string) bool { 53 | return isValidCryptocurrencyAddress("btc", address) 54 | } 55 | 56 | // IsValidETHAddress 是否有效的ETH地址 57 | // 简称:ERC20 58 | func IsValidETHAddress(address string) bool { 59 | return isValidCryptocurrencyAddress("eth", address) 60 | } 61 | 62 | // IsValidTRONAddress 是否有效的TRON地址 63 | // 简称:TRC20 64 | func IsValidTRONAddress(address string) bool { 65 | return isValidCryptocurrencyAddress("trc", address) 66 | } 67 | 68 | func isValidCryptocurrencyAddress(crypto, address string) bool { 69 | if len(address) == 0 { 70 | return false 71 | } 72 | 73 | item, ok := cryptoRegexMap[crypto] 74 | if !ok { 75 | return false 76 | } 77 | 78 | if item.MatchString(address) { 79 | return true 80 | } 81 | 82 | return false 83 | } 84 | 85 | // IsValidCryptocurrencyAddress 校验加密货币钱包地址 86 | func IsValidCryptocurrencyAddress(address string) string { 87 | if len(address) == 0 { 88 | return "" 89 | } 90 | 91 | for k, re := range cryptoRegexMap { 92 | if re.MatchString(address) { 93 | return k 94 | } 95 | } 96 | 97 | return "" 98 | } 99 | -------------------------------------------------------------------------------- /cryptocurrency/cryptocurrency_test.go: -------------------------------------------------------------------------------- 1 | package cryptocurrency 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestIsValidCryptocurrencyAddress(t *testing.T) { 11 | type walletAddressInput struct { 12 | Crypto string 13 | Address string 14 | } 15 | 16 | var validWalletAddresses = []walletAddressInput{ 17 | {Crypto: "btc", Address: "1CFNjwLjZdSKB8nZopxhLaR8vvqaQKD3Bi"}, //old btc type 18 | {Crypto: "BTC", Address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"}, 19 | {Crypto: "BTC", Address: "1RAHUEYstWetqabcFn5Au4m4GFg7xJaNVN2"}, 20 | {Crypto: "BTC", Address: "3J98t1RHT73CNmQwertyyWrnqRhWNLy"}, 21 | {Crypto: "BTC", Address: "bc1qarsrrr7ASHy5643ydab9re59gtzzwfrah"}, 22 | // {Crypto: "bch", Address: "qq7ujnfl6tqx7xcdsdsrsqlqgqz8rm5stsvgx2kcvu"}, // cash address 23 | // {Crypto: "bch", Address: "bitcoincash:qq7ujnfl6tqx7xcdsdsrsqlqgqz8rm5stsvgx2kcvu"}, // cash address 24 | // {Crypto: "bch", Address: "16dhNPnPp346wzrRTkArKhqPM1ELeJDvRr"}, 25 | {Crypto: "BTG", Address: "GakMJVF7Du16VK9dpN6nhJyLUPLXkTfqSY"}, 26 | {Crypto: "DGB", Address: "D59P8MiMXkjs7HPn31zAnUSvRNwvNZUBYa"}, 27 | {Crypto: "DASH", Address: "XiHMBEic8q8wX5aKqVv6zRFec7cAuYGjBV"}, 28 | {Crypto: "ETH", Address: "0x15cc4bf4fe84fea178d2b10f89f1a6c914dfc8c2"}, 29 | {Crypto: "ETH", Address: "0x323b5d4c32345ced77393b3530b1eed0f346429d"}, 30 | {Crypto: "ETH", Address: "0xZYXb5d4c32345ced77393b3530b1eed0f346429d"}, 31 | {Crypto: "ETH", Address: "0xe41d2489571d322189246dafa5ebde1f4699f498"}, 32 | {Crypto: "ETH", Address: "0x8e215d06ea7ec1fdb4fc5fd21768f4b34ee92ef4"}, 33 | {Crypto: "SMART", Address: "SbsLb8eM583oraW89qhbkcqZmuR4aYKkea"}, 34 | {Crypto: "XRP", Address: "rMkfgicNKuCfXojDhcX4W2LnGoHFqhFrr6"}, 35 | {Crypto: "ZEC", Address: "t1SBt3V8MfG4ZJ2ZDTuWfDshn4PuyvqjJV3"}, 36 | {Crypto: "ZCR", Address: "ZXvpr2M6wvKoFcTJ57WCjT9Wkd38xkL8Fo"}, 37 | {Crypto: "trc", Address: "TC74QG8tbtixG5Raa4fEifywgjrFs45fNz"}, 38 | {Crypto: "trc", Address: "TFUD8x3iAZ9dF7NDCGBtSjznemEomE5rP9"}, 39 | {Crypto: "trc", Address: "TPcKtz5TRfP4xUZSos81RmXB9K2DBqj2iu"}, 40 | } 41 | 42 | var invalidWalletAddresses = []walletAddressInput{ 43 | {Crypto: "btc", Address: "2CFNjwLjZdSKB8nZopxhLaR8vvqaQKD3Bi"}, 44 | {Crypto: "BTC", Address: "bc2qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"}, 45 | {Crypto: "BTC", Address: "b1qarsrrr7ASHy5643ydab9re59gtzzwfrah"}, 46 | {Crypto: "BTC", Address: "0J98t1RHT73CNmQwertyyWrnqRhWNLy"}, 47 | {Crypto: "BTG", Address: "DakMJVF7Du16VK9dpN6nhJyLUPLXkTfqSY"}, 48 | {Crypto: "DGB", Address: "G59P8MiMXkjs7HPn31zAnUSvRNwvNZUBYa"}, 49 | {Crypto: "DASH", Address: "QiHMBEic8q8wX5aKqVv6zRFec7cAuYGjBV"}, 50 | {Crypto: "ETH", Address: "1x15cc4bf4fe84fea178d2b10f89f1a6c914dfc8c2"}, 51 | {Crypto: "SMART", Address: "sbsLb8eM583oraW89qhbkcqZmuR4aYKkea"}, 52 | {Crypto: "XRP", Address: "RMkfgicNKuCfXojDhcX4W2LnGoHFqhFrr6"}, 53 | {Crypto: "ZEC", Address: "z1SBt3V8MfG4ZJ2ZDTuWfDshn4PuyvqjJV3"}, 54 | {Crypto: "ZCR", Address: "zXvpr2M6wvKoFcTJ57WCjT9Wkd38xkL8Fo"}, 55 | } 56 | 57 | for _, w := range validWalletAddresses { 58 | t.Run("valid address "+w.Crypto, func(t *testing.T) { 59 | result := IsValidCryptocurrencyAddress(w.Address) 60 | assert.True(t, strings.Compare(strings.ToLower(w.Crypto), result) == 0) 61 | }) 62 | } 63 | 64 | for _, w := range invalidWalletAddresses { 65 | t.Run("invalid address "+w.Crypto, func(t *testing.T) { 66 | result := IsValidCryptocurrencyAddress(w.Address) 67 | assert.False(t, strings.Compare(strings.ToLower(w.Crypto), result) == 0) 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /dateutil/dateutils.go: -------------------------------------------------------------------------------- 1 | package dateutil 2 | 3 | import "time" 4 | 5 | // Floor - takes a datetime and return a datetime from the same day at 00:00:00 (UTC). 6 | func Floor(t time.Time) time.Time { 7 | return t.UTC().Truncate(time.Hour * 24) 8 | } 9 | 10 | // Ceil - takes a datetime and return a datetime from the same day at 23:59:59 (UTC). 11 | func Ceil(t time.Time) time.Time { 12 | // add 24 hours so that we are dealing with tomorrow's datetime 13 | // Floor 14 | // Substract one second and we have today at 23:59:59 15 | return Floor(t.Add(time.Hour * 24)).Add(time.Second * -1) 16 | } 17 | 18 | func BeforeOrEqual(milestone time.Time, date time.Time) bool { 19 | return date.UTC().Before(milestone) || date.UTC().Equal(milestone) 20 | } 21 | 22 | func AfterOrEqual(milestone time.Time, date time.Time) bool { 23 | return date.UTC().After(milestone) || date.UTC().Equal(milestone) 24 | } 25 | 26 | // Overlap - returns true if two date intervals overlap. 27 | func Overlap(start1 time.Time, end1 time.Time, start2 time.Time, end2 time.Time) bool { 28 | return (AfterOrEqual(start2, start1) && BeforeOrEqual(end2, start1)) || 29 | (AfterOrEqual(start2, end1) && BeforeOrEqual(end2, end1)) || 30 | (AfterOrEqual(start1, start2) && BeforeOrEqual(end1, start2)) || 31 | (AfterOrEqual(start1, end2) && BeforeOrEqual(end1, end2)) 32 | } 33 | -------------------------------------------------------------------------------- /dateutil/dateutils_test.go: -------------------------------------------------------------------------------- 1 | package dateutil_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/tx7do/go-utils/dateutil" 9 | ) 10 | 11 | func TestFloor(t *testing.T) { 12 | now := time.Now() 13 | assert.Equal(t, "00:00:00", dateutil.Floor(now).Format("15:04:05")) 14 | } 15 | 16 | func TestCeil(t *testing.T) { 17 | now := time.Now() 18 | assert.Equal(t, "23:59:59", dateutil.Ceil(now).Format("15:04:05")) 19 | } 20 | 21 | func TestBeforeOrEqual(t *testing.T) { 22 | milestone, _ := time.Parse("2006-01-02", "2023-01-01") 23 | 24 | dBefore, _ := time.Parse("2006-01-02", "2022-12-31") 25 | dEqual, _ := time.Parse("2006-01-02", "2023-01-01") 26 | dAfter, _ := time.Parse("2006-01-02", "2023-01-31") 27 | 28 | assert.Equal(t, true, dateutil.BeforeOrEqual(milestone, dBefore)) 29 | assert.Equal(t, true, dateutil.BeforeOrEqual(milestone, dEqual)) 30 | assert.Equal(t, false, dateutil.BeforeOrEqual(milestone, dAfter)) 31 | } 32 | 33 | func TestAfterOrEqual(t *testing.T) { 34 | milestone, _ := time.Parse("2006-01-02", "2023-01-01") 35 | 36 | dBefore, _ := time.Parse("2006-01-02", "2022-12-31") 37 | dEqual, _ := time.Parse("2006-01-02", "2023-01-01") 38 | dAfter, _ := time.Parse("2006-01-02", "2023-01-31") 39 | 40 | assert.Equal(t, false, dateutil.AfterOrEqual(milestone, dBefore)) 41 | assert.Equal(t, true, dateutil.AfterOrEqual(milestone, dEqual)) 42 | assert.Equal(t, true, dateutil.AfterOrEqual(milestone, dAfter)) 43 | } 44 | 45 | func TestOverlap(t *testing.T) { 46 | s1, _ := time.Parse("2006-01-02", "2022-12-28") 47 | e1, _ := time.Parse("2006-01-02", "2022-12-31") 48 | 49 | s2, _ := time.Parse("2006-01-02", "2022-12-30") 50 | e2, _ := time.Parse("2006-01-02", "2023-01-01") 51 | 52 | s3, _ := time.Parse("2006-01-02", "2023-01-02") 53 | e3, _ := time.Parse("2006-01-02", "2023-01-04") 54 | 55 | assert.Equal(t, true, dateutil.Overlap(s1, e1, s2, e2)) 56 | assert.Equal(t, false, dateutil.Overlap(s1, e1, s3, e3)) 57 | 58 | s4, _ := time.Parse("2006-01-02", "2023-07-13") 59 | e4, _ := time.Parse("2006-01-02", "2023-07-14") 60 | 61 | s5, _ := time.Parse("2006-01-02", "2023-07-10") 62 | e5, _ := time.Parse("2006-01-02", "2023-07-17") 63 | 64 | assert.Equal(t, true, dateutil.Overlap(s4, e4, s5, e5)) 65 | assert.Equal(t, true, dateutil.Overlap(s5, e5, s4, e4)) 66 | } 67 | -------------------------------------------------------------------------------- /entgo/ent_client.go: -------------------------------------------------------------------------------- 1 | package entgo 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "go.opentelemetry.io/otel/attribute" 11 | semconv "go.opentelemetry.io/otel/semconv/v1.10.0" 12 | 13 | "github.com/XSAM/otelsql" 14 | 15 | entSql "entgo.io/ent/dialect/sql" 16 | ) 17 | 18 | type EntClientInterface interface { 19 | Close() error 20 | } 21 | 22 | type EntClient[T EntClientInterface] struct { 23 | db T 24 | drv *entSql.Driver 25 | } 26 | 27 | func NewEntClient[T EntClientInterface](db T, drv *entSql.Driver) *EntClient[T] { 28 | return &EntClient[T]{ 29 | db: db, 30 | drv: drv, 31 | } 32 | } 33 | 34 | func (c *EntClient[T]) Client() T { 35 | return c.db 36 | } 37 | 38 | func (c *EntClient[T]) Driver() *entSql.Driver { 39 | return c.drv 40 | } 41 | 42 | func (c *EntClient[T]) DB() *sql.DB { 43 | return c.drv.DB() 44 | } 45 | 46 | // Close 关闭数据库连接 47 | func (c *EntClient[T]) Close() error { 48 | return c.db.Close() 49 | } 50 | 51 | // Query 查询数据 52 | func (c *EntClient[T]) Query(ctx context.Context, query string, args, v any) error { 53 | return c.Driver().Query(ctx, query, args, v) 54 | } 55 | 56 | func (c *EntClient[T]) Exec(ctx context.Context, query string, args, v any) error { 57 | return c.Driver().Exec(ctx, query, args, v) 58 | } 59 | 60 | // SetConnectionOption 设置连接配置 61 | func (c *EntClient[T]) SetConnectionOption(maxIdleConnections, maxOpenConnections int, connMaxLifetime time.Duration) { 62 | // 连接池中最多保留的空闲连接数量 63 | c.DB().SetMaxIdleConns(maxIdleConnections) 64 | // 连接池在同一时间打开连接的最大数量 65 | c.DB().SetMaxOpenConns(maxOpenConnections) 66 | // 连接可重用的最大时间长度 67 | c.DB().SetConnMaxLifetime(connMaxLifetime) 68 | } 69 | 70 | func driverNameToSemConvKeyValue(driverName string) attribute.KeyValue { 71 | switch driverName { 72 | case "mariadb": 73 | return semconv.DBSystemMariaDB 74 | case "mysql": 75 | return semconv.DBSystemMySQL 76 | case "postgresql": 77 | return semconv.DBSystemPostgreSQL 78 | case "sqlite": 79 | return semconv.DBSystemSqlite 80 | default: 81 | return semconv.DBSystemKey.String(driverName) 82 | } 83 | } 84 | 85 | // CreateDriver 创建数据库驱动 86 | func CreateDriver(driverName, dsn string, enableTrace, enableMetrics bool) (*entSql.Driver, error) { 87 | var db *sql.DB 88 | var drv *entSql.Driver 89 | var err error 90 | 91 | if enableTrace { 92 | // Connect to database 93 | if db, err = otelsql.Open(driverName, dsn, otelsql.WithAttributes( 94 | driverNameToSemConvKeyValue(driverName), 95 | )); err != nil { 96 | return nil, errors.New(fmt.Sprintf("failed opening connection to db: %v", err)) 97 | } 98 | 99 | drv = entSql.OpenDB(driverName, db) 100 | } else { 101 | if drv, err = entSql.Open(driverName, dsn); err != nil { 102 | return nil, errors.New(fmt.Sprintf("failed opening connection to db: %v", err)) 103 | } 104 | 105 | db = drv.DB() 106 | } 107 | 108 | // Register DB stats to meter 109 | if enableMetrics { 110 | err = otelsql.RegisterDBStatsMetrics(db, otelsql.WithAttributes( 111 | driverNameToSemConvKeyValue(driverName), 112 | )) 113 | if err != nil { 114 | return nil, errors.New(fmt.Sprintf("failed register otel meter: %v", err)) 115 | } 116 | } 117 | 118 | return drv, nil 119 | } 120 | -------------------------------------------------------------------------------- /entgo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tx7do/go-utils/entgo 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | replace github.com/tx7do/go-utils/id => ../id 8 | 9 | require ( 10 | entgo.io/contrib v0.6.0 11 | entgo.io/ent v0.14.4 12 | github.com/XSAM/otelsql v0.38.0 13 | github.com/go-kratos/kratos/v2 v2.8.4 14 | github.com/google/uuid v1.6.0 15 | github.com/stretchr/testify v1.10.0 16 | github.com/tx7do/go-utils v1.1.28 17 | github.com/tx7do/go-utils/id v0.0.2 18 | go.opentelemetry.io/otel v1.36.0 19 | google.golang.org/protobuf v1.36.6 20 | ) 21 | 22 | require ( 23 | ariga.io/atlas v0.34.0 // indirect 24 | github.com/agext/levenshtein v1.2.3 // indirect 25 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 26 | github.com/bmatcuk/doublestar v1.3.4 // indirect 27 | github.com/bufbuild/protocompile v0.14.1 // indirect 28 | github.com/bwmarrin/snowflake v0.3.0 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/go-logr/logr v1.4.3 // indirect 31 | github.com/go-logr/stdr v1.2.2 // indirect 32 | github.com/go-openapi/inflect v0.21.2 // indirect 33 | github.com/golang/protobuf v1.5.4 // indirect 34 | github.com/google/go-cmp v0.7.0 // indirect 35 | github.com/hashicorp/hcl/v2 v2.23.0 // indirect 36 | github.com/jhump/protoreflect v1.17.0 // indirect 37 | github.com/lithammer/shortuuid/v4 v4.2.0 // indirect 38 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 39 | github.com/mitchellh/mapstructure v1.5.0 // indirect 40 | github.com/pmezard/go-difflib v1.0.0 // indirect 41 | github.com/rs/xid v1.6.0 // indirect 42 | github.com/segmentio/ksuid v1.0.4 // indirect 43 | github.com/sony/sonyflake v1.2.1 // indirect 44 | github.com/zclconf/go-cty v1.16.3 // indirect 45 | github.com/zclconf/go-cty-yaml v1.1.0 // indirect 46 | go.mongodb.org/mongo-driver v1.17.3 // indirect 47 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 48 | go.opentelemetry.io/otel/metric v1.36.0 // indirect 49 | go.opentelemetry.io/otel/trace v1.36.0 // indirect 50 | go.uber.org/multierr v1.11.0 // indirect 51 | golang.org/x/mod v0.24.0 // indirect 52 | golang.org/x/sync v0.14.0 // indirect 53 | golang.org/x/text v0.25.0 // indirect 54 | golang.org/x/tools v0.33.0 // indirect 55 | gopkg.in/yaml.v3 v3.0.1 // indirect 56 | ) 57 | 58 | replace github.com/tx7do/go-utils => ../ 59 | -------------------------------------------------------------------------------- /entgo/mixin/autoincrement_id.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "entgo.io/contrib/entproto" 5 | "entgo.io/ent" 6 | "entgo.io/ent/dialect" 7 | "entgo.io/ent/schema/field" 8 | "entgo.io/ent/schema/index" 9 | "entgo.io/ent/schema/mixin" 10 | ) 11 | 12 | var _ ent.Mixin = (*AutoIncrementId)(nil) 13 | 14 | type AutoIncrementId struct{ mixin.Schema } 15 | 16 | func (AutoIncrementId) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.Uint32("id"). 19 | Comment("id"). 20 | StructTag(`json:"id,omitempty"`). 21 | SchemaType(map[string]string{ 22 | dialect.MySQL: "int", 23 | dialect.Postgres: "serial", 24 | }). 25 | Annotations( 26 | entproto.Field(1), 27 | ). 28 | Positive(). 29 | Immutable(). 30 | Unique(), 31 | } 32 | } 33 | 34 | // Indexes of the AutoIncrementId. 35 | func (AutoIncrementId) Indexes() []ent.Index { 36 | return []ent.Index{ 37 | index.Fields("id"), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /entgo/mixin/creator_id.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | _mixin "entgo.io/ent/schema/mixin" 7 | ) 8 | 9 | type CreatorId struct { 10 | _mixin.Schema 11 | } 12 | 13 | func (CreatorId) Fields() []ent.Field { 14 | return []ent.Field{ 15 | field.Uint64("creator_id"). 16 | Comment("创建者用户ID"). 17 | Immutable(). 18 | Optional(). 19 | Nillable(), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /entgo/mixin/operator.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | "entgo.io/ent/schema/mixin" 7 | ) 8 | 9 | var _ ent.Mixin = (*CreateBy)(nil) 10 | 11 | type CreateBy struct{ mixin.Schema } 12 | 13 | func (CreateBy) Fields() []ent.Field { 14 | return []ent.Field{ 15 | field.Uint32("create_by"). 16 | Comment("创建者ID"). 17 | Optional(). 18 | Nillable(), 19 | } 20 | } 21 | 22 | var _ ent.Mixin = (*UpdateBy)(nil) 23 | 24 | type UpdateBy struct{ mixin.Schema } 25 | 26 | func (UpdateBy) Fields() []ent.Field { 27 | return []ent.Field{ 28 | field.Uint32("update_by"). 29 | Comment("更新者ID"). 30 | Optional(). 31 | Nillable(), 32 | } 33 | } 34 | 35 | var _ ent.Mixin = (*DeleteBy)(nil) 36 | 37 | type DeleteBy struct{ mixin.Schema } 38 | 39 | func (DeleteBy) Fields() []ent.Field { 40 | return []ent.Field{ 41 | field.Uint32("delete_by"). 42 | Comment("删除者ID"). 43 | Optional(). 44 | Nillable(), 45 | } 46 | } 47 | 48 | var _ ent.Mixin = (*CreatedBy)(nil) 49 | 50 | type CreatedBy struct{ mixin.Schema } 51 | 52 | func (CreatedBy) Fields() []ent.Field { 53 | return []ent.Field{ 54 | field.Uint32("created_by"). 55 | Comment("创建者ID"). 56 | Optional(). 57 | Nillable(), 58 | } 59 | } 60 | 61 | var _ ent.Mixin = (*UpdatedBy)(nil) 62 | 63 | type UpdatedBy struct{ mixin.Schema } 64 | 65 | func (UpdatedBy) Fields() []ent.Field { 66 | return []ent.Field{ 67 | field.Uint32("updated_by"). 68 | Comment("更新者ID"). 69 | Optional(). 70 | Nillable(), 71 | } 72 | } 73 | 74 | var _ ent.Mixin = (*DeletedBy)(nil) 75 | 76 | type DeletedBy struct{ mixin.Schema } 77 | 78 | func (DeletedBy) Fields() []ent.Field { 79 | return []ent.Field{ 80 | field.Uint32("deleted_by"). 81 | Comment("删除者ID"). 82 | Optional(). 83 | Nillable(), 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /entgo/mixin/remark.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | "entgo.io/ent/schema/mixin" 7 | ) 8 | 9 | type Remark struct { 10 | mixin.Schema 11 | } 12 | 13 | func (Remark) Fields() []ent.Field { 14 | return []ent.Field{ 15 | field.String("remark"). 16 | Comment("备注"). 17 | Default(""). 18 | Optional(). 19 | Nillable(), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /entgo/mixin/snowflake_id.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/dialect" 6 | "entgo.io/ent/schema/field" 7 | "entgo.io/ent/schema/index" 8 | "entgo.io/ent/schema/mixin" 9 | 10 | "github.com/tx7do/go-utils/id" 11 | ) 12 | 13 | type SnowflackId struct { 14 | mixin.Schema 15 | } 16 | 17 | func (SnowflackId) Fields() []ent.Field { 18 | return []ent.Field{ 19 | field.Uint64("id"). 20 | Comment("id"). 21 | DefaultFunc(id.GenerateSonyflakeID). 22 | Positive(). 23 | Immutable(). 24 | StructTag(`json:"id,omitempty"`). 25 | SchemaType(map[string]string{ 26 | dialect.MySQL: "bigint", 27 | dialect.Postgres: "bigint", 28 | }), 29 | } 30 | } 31 | 32 | // Indexes of the SnowflackId. 33 | func (SnowflackId) Indexes() []ent.Index { 34 | return []ent.Index{ 35 | index.Fields("id"), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /entgo/mixin/string_id.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | "entgo.io/ent/schema/index" 7 | "entgo.io/ent/schema/mixin" 8 | "regexp" 9 | ) 10 | 11 | type StringId struct { 12 | mixin.Schema 13 | } 14 | 15 | func (StringId) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("id"). 18 | Comment("id"). 19 | MaxLen(25). 20 | NotEmpty(). 21 | Unique(). 22 | Immutable(). 23 | Match(regexp.MustCompile("^[0-9a-zA-Z_\\-]+$")). 24 | StructTag(`json:"id,omitempty"`), 25 | } 26 | } 27 | 28 | // Indexes of the StringId. 29 | func (StringId) Indexes() []ent.Index { 30 | return []ent.Index{ 31 | index.Fields("id"), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /entgo/mixin/switch_status.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | "entgo.io/ent/schema/mixin" 7 | ) 8 | 9 | type SwitchStatus struct { 10 | mixin.Schema 11 | } 12 | 13 | func (SwitchStatus) Fields() []ent.Field { 14 | return []ent.Field{ 15 | /** 16 | 在PostgreSQL下,还需要为此创建一个Type,否则无法使用。 17 | 18 | DROP TYPE IF EXISTS switch_status CASCADE; 19 | CREATE TYPE switch_status AS ENUM ( 20 | 'OFF', 21 | 'ON' 22 | ); 23 | */ 24 | field.Enum("status"). 25 | Comment("状态"). 26 | Optional(). 27 | Nillable(). 28 | //SchemaType(map[string]string{ 29 | // dialect.MySQL: "switch_status", 30 | // dialect.Postgres: "switch_status", 31 | //}). 32 | Default("ON"). 33 | Values( 34 | "OFF", 35 | "ON", 36 | ), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /entgo/mixin/time.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | "entgo.io/ent/schema/mixin" 7 | ) 8 | 9 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 10 | 11 | var _ ent.Mixin = (*CreatedAt)(nil) 12 | 13 | type CreatedAt struct{ mixin.Schema } 14 | 15 | func (CreatedAt) Fields() []ent.Field { 16 | return []ent.Field{ 17 | // 创建时间 18 | field.Time("created_at"). 19 | Comment("创建时间"). 20 | Immutable(). 21 | Optional(). 22 | Nillable(), 23 | } 24 | } 25 | 26 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 27 | 28 | var _ ent.Mixin = (*UpdatedAt)(nil) 29 | 30 | type UpdatedAt struct{ mixin.Schema } 31 | 32 | func (UpdatedAt) Fields() []ent.Field { 33 | return []ent.Field{ 34 | // 更新时间 35 | field.Time("updated_at"). 36 | Comment("更新时间"). 37 | Optional(). 38 | Nillable(), 39 | } 40 | } 41 | 42 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 43 | 44 | var _ ent.Mixin = (*DeletedAt)(nil) 45 | 46 | type DeletedAt struct{ mixin.Schema } 47 | 48 | func (DeletedAt) Fields() []ent.Field { 49 | return []ent.Field{ 50 | // 删除时间 51 | field.Time("deleted_at"). 52 | Comment("删除时间"). 53 | Optional(). 54 | Nillable(), 55 | } 56 | } 57 | 58 | var _ ent.Mixin = (*TimeAt)(nil) 59 | 60 | type TimeAt struct{ mixin.Schema } 61 | 62 | func (TimeAt) Fields() []ent.Field { 63 | var fields []ent.Field 64 | fields = append(fields, CreatedAt{}.Fields()...) 65 | fields = append(fields, UpdatedAt{}.Fields()...) 66 | fields = append(fields, DeletedAt{}.Fields()...) 67 | return fields 68 | } 69 | 70 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 71 | 72 | var _ ent.Mixin = (*CreateTime)(nil) 73 | 74 | type CreateTime struct{ mixin.Schema } 75 | 76 | func (CreateTime) Fields() []ent.Field { 77 | return []ent.Field{ 78 | // 创建时间 79 | field.Time("create_time"). 80 | Comment("创建时间"). 81 | Immutable(). 82 | Optional(). 83 | Nillable(), 84 | } 85 | } 86 | 87 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 88 | 89 | var _ ent.Mixin = (*UpdateTime)(nil) 90 | 91 | type UpdateTime struct{ mixin.Schema } 92 | 93 | func (UpdateTime) Fields() []ent.Field { 94 | return []ent.Field{ 95 | // 更新时间 96 | field.Time("update_time"). 97 | Comment("更新时间"). 98 | Optional(). 99 | Nillable(), 100 | } 101 | } 102 | 103 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 104 | 105 | var _ ent.Mixin = (*DeleteTime)(nil) 106 | 107 | type DeleteTime struct{ mixin.Schema } 108 | 109 | func (DeleteTime) Fields() []ent.Field { 110 | return []ent.Field{ 111 | // 删除时间 112 | field.Time("delete_time"). 113 | Comment("删除时间"). 114 | Optional(). 115 | Nillable(), 116 | } 117 | } 118 | 119 | var _ ent.Mixin = (*Time)(nil) 120 | 121 | type Time struct{ mixin.Schema } 122 | 123 | func (Time) Fields() []ent.Field { 124 | var fields []ent.Field 125 | fields = append(fields, CreateTime{}.Fields()...) 126 | fields = append(fields, UpdateTime{}.Fields()...) 127 | fields = append(fields, DeleteTime{}.Fields()...) 128 | return fields 129 | } 130 | 131 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 132 | -------------------------------------------------------------------------------- /entgo/mixin/timestamp.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "time" 5 | 6 | "entgo.io/ent" 7 | "entgo.io/ent/schema/field" 8 | "entgo.io/ent/schema/mixin" 9 | ) 10 | 11 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 12 | 13 | var _ ent.Mixin = (*CreateTimestamp)(nil) 14 | 15 | type CreateTimestamp struct{ mixin.Schema } 16 | 17 | func (CreateTimestamp) Fields() []ent.Field { 18 | return []ent.Field{ 19 | // 创建时间,毫秒 20 | field.Int64("create_time"). 21 | Comment("创建时间"). 22 | Immutable(). 23 | Optional(). 24 | Nillable(). 25 | DefaultFunc(time.Now().UnixMilli), 26 | } 27 | } 28 | 29 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 30 | 31 | var _ ent.Mixin = (*UpdateTimestamp)(nil) 32 | 33 | type UpdateTimestamp struct{ mixin.Schema } 34 | 35 | func (UpdateTimestamp) Fields() []ent.Field { 36 | return []ent.Field{ 37 | // 更新时间,毫秒 38 | // 需要注意的是,如果不是程序自动更新,那么这个字段不会被更新,除非在数据库里面下触发器更新。 39 | field.Int64("update_time"). 40 | Comment("更新时间"). 41 | Optional(). 42 | Nillable(). 43 | UpdateDefault(time.Now().UnixMilli), 44 | } 45 | } 46 | 47 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 48 | 49 | var _ ent.Mixin = (*DeleteTimestamp)(nil) 50 | 51 | type DeleteTimestamp struct{ mixin.Schema } 52 | 53 | func (DeleteTimestamp) Fields() []ent.Field { 54 | return []ent.Field{ 55 | // 删除时间,毫秒 56 | field.Int64("delete_time"). 57 | Comment("删除时间"). 58 | Optional(). 59 | Nillable(), 60 | } 61 | } 62 | 63 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 64 | 65 | var _ ent.Mixin = (*Timestamp)(nil) 66 | 67 | type Timestamp struct{ mixin.Schema } 68 | 69 | func (Timestamp) Fields() []ent.Field { 70 | var fields []ent.Field 71 | fields = append(fields, CreateTimestamp{}.Fields()...) 72 | fields = append(fields, UpdateTimestamp{}.Fields()...) 73 | fields = append(fields, DeleteTimestamp{}.Fields()...) 74 | return fields 75 | } 76 | 77 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 78 | 79 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 80 | 81 | var _ ent.Mixin = (*CreatedAtTimestamp)(nil) 82 | 83 | type CreatedAtTimestamp struct{ mixin.Schema } 84 | 85 | func (CreatedAtTimestamp) Fields() []ent.Field { 86 | return []ent.Field{ 87 | // 创建时间,毫秒 88 | field.Int64("created_at"). 89 | Comment("创建时间"). 90 | Immutable(). 91 | Optional(). 92 | Nillable(). 93 | DefaultFunc(time.Now().UnixMilli), 94 | } 95 | } 96 | 97 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 98 | 99 | var _ ent.Mixin = (*UpdatedAtTimestamp)(nil) 100 | 101 | type UpdatedAtTimestamp struct{ mixin.Schema } 102 | 103 | func (UpdatedAtTimestamp) Fields() []ent.Field { 104 | return []ent.Field{ 105 | // 更新时间,毫秒 106 | // 需要注意的是,如果不是程序自动更新,那么这个字段不会被更新,除非在数据库里面下触发器更新。 107 | field.Int64("updated_at"). 108 | Comment("更新时间"). 109 | Optional(). 110 | Nillable(). 111 | UpdateDefault(time.Now().UnixMilli), 112 | } 113 | } 114 | 115 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 116 | 117 | var _ ent.Mixin = (*DeletedAtTimestamp)(nil) 118 | 119 | type DeletedAtTimestamp struct{ mixin.Schema } 120 | 121 | func (DeletedAtTimestamp) Fields() []ent.Field { 122 | return []ent.Field{ 123 | // 删除时间,毫秒 124 | field.Int64("deleted_at"). 125 | Comment("删除时间"). 126 | Optional(). 127 | Nillable(), 128 | } 129 | } 130 | 131 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 132 | 133 | var _ ent.Mixin = (*TimestampAt)(nil) 134 | 135 | type TimestampAt struct{ mixin.Schema } 136 | 137 | func (TimestampAt) Fields() []ent.Field { 138 | var fields []ent.Field 139 | fields = append(fields, CreatedAtTimestamp{}.Fields()...) 140 | fields = append(fields, UpdatedAtTimestamp{}.Fields()...) 141 | fields = append(fields, DeletedAtTimestamp{}.Fields()...) 142 | return fields 143 | } 144 | 145 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 146 | -------------------------------------------------------------------------------- /entgo/mixin/uuid_id.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | "entgo.io/ent/schema/index" 7 | "entgo.io/ent/schema/mixin" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | var _ ent.Mixin = (*UuidId)(nil) 12 | 13 | type UuidId struct{ mixin.Schema } 14 | 15 | func (UuidId) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.UUID("id", uuid.UUID{}). 18 | Comment("id"). 19 | Default(uuid.New). 20 | Unique(). 21 | Immutable(). 22 | StructTag(`json:"id,omitempty"`), 23 | } 24 | } 25 | 26 | // Indexes of the UuidId. 27 | func (UuidId) Indexes() []ent.Index { 28 | return []ent.Index{ 29 | index.Fields("id"), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /entgo/query/order.go: -------------------------------------------------------------------------------- 1 | package entgo 2 | 3 | import ( 4 | "strings" 5 | 6 | "entgo.io/ent/dialect/sql" 7 | ) 8 | 9 | // QueryCommandToOrderConditions 查询命令转换为排序条件 10 | func QueryCommandToOrderConditions(orderBys []string) (error, func(s *sql.Selector)) { 11 | if len(orderBys) == 0 { 12 | return nil, nil 13 | } 14 | 15 | return nil, func(s *sql.Selector) { 16 | for _, v := range orderBys { 17 | if strings.HasPrefix(v, "-") { 18 | // 降序 19 | key := v[1:] 20 | if len(key) == 0 { 21 | continue 22 | } 23 | 24 | BuildOrderSelect(s, key, true) 25 | } else { 26 | // 升序 27 | if len(v) == 0 { 28 | continue 29 | } 30 | 31 | BuildOrderSelect(s, v, false) 32 | } 33 | } 34 | } 35 | } 36 | 37 | func BuildOrderSelect(s *sql.Selector, field string, desc bool) { 38 | if desc { 39 | s.OrderBy(sql.Desc(s.C(field))) 40 | } else { 41 | s.OrderBy(sql.Asc(s.C(field))) 42 | } 43 | } 44 | 45 | func BuildOrderSelector(orderBys []string, defaultOrderField string) (error, func(s *sql.Selector)) { 46 | if len(orderBys) == 0 { 47 | return nil, func(s *sql.Selector) { 48 | BuildOrderSelect(s, defaultOrderField, true) 49 | } 50 | } else { 51 | return QueryCommandToOrderConditions(orderBys) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /entgo/query/pagination.go: -------------------------------------------------------------------------------- 1 | package entgo 2 | 3 | import ( 4 | "entgo.io/ent/dialect/sql" 5 | 6 | paging "github.com/tx7do/go-utils/pagination" 7 | ) 8 | 9 | func BuildPaginationSelector(page, pageSize int32, noPaging bool) func(*sql.Selector) { 10 | if noPaging { 11 | return nil 12 | } else { 13 | return func(s *sql.Selector) { 14 | BuildPaginationSelect(s, page, pageSize) 15 | } 16 | } 17 | } 18 | 19 | func BuildPaginationSelect(s *sql.Selector, page, pageSize int32) { 20 | if page < 1 { 21 | page = paging.DefaultPage 22 | } 23 | 24 | if pageSize < 1 { 25 | pageSize = paging.DefaultPageSize 26 | } 27 | offset := paging.GetPageOffset(page, pageSize) 28 | s.Offset(offset).Limit(int(pageSize)) 29 | } 30 | -------------------------------------------------------------------------------- /entgo/query/query.go: -------------------------------------------------------------------------------- 1 | package entgo 2 | 3 | import ( 4 | "entgo.io/ent/dialect/sql" 5 | 6 | _ "github.com/go-kratos/kratos/v2/encoding/json" 7 | ) 8 | 9 | // BuildQuerySelector 构建分页过滤查询器 10 | func BuildQuerySelector( 11 | andFilterJsonString, orFilterJsonString string, 12 | page, pageSize int32, noPaging bool, 13 | orderBys []string, defaultOrderField string, 14 | selectFields []string, 15 | ) (err error, whereSelectors []func(s *sql.Selector), querySelectors []func(s *sql.Selector)) { 16 | err, whereSelectors = BuildFilterSelector(andFilterJsonString, orFilterJsonString) 17 | if err != nil { 18 | return err, nil, nil 19 | } 20 | 21 | var orderSelector func(s *sql.Selector) 22 | err, orderSelector = BuildOrderSelector(orderBys, defaultOrderField) 23 | if err != nil { 24 | return err, nil, nil 25 | } 26 | 27 | pageSelector := BuildPaginationSelector(page, pageSize, noPaging) 28 | 29 | var fieldSelector func(s *sql.Selector) 30 | err, fieldSelector = BuildFieldSelector(selectFields) 31 | 32 | if len(whereSelectors) > 0 { 33 | querySelectors = append(querySelectors, whereSelectors...) 34 | } 35 | 36 | if orderSelector != nil { 37 | querySelectors = append(querySelectors, orderSelector) 38 | } 39 | if pageSelector != nil { 40 | querySelectors = append(querySelectors, pageSelector) 41 | } 42 | if fieldSelector != nil { 43 | querySelectors = append(querySelectors, fieldSelector) 44 | } 45 | 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /entgo/query/select.go: -------------------------------------------------------------------------------- 1 | package entgo 2 | 3 | import ( 4 | "entgo.io/ent/dialect/sql" 5 | "github.com/tx7do/go-utils/stringcase" 6 | ) 7 | 8 | func BuildFieldSelect(s *sql.Selector, fields []string) { 9 | if len(fields) > 0 { 10 | for i, field := range fields { 11 | switch { 12 | case field == "id_" || field == "_id": 13 | field = "id" 14 | } 15 | fields[i] = stringcase.ToSnakeCase(field) 16 | } 17 | s.Select(fields...) 18 | } 19 | } 20 | 21 | func BuildFieldSelector(fields []string) (error, func(s *sql.Selector)) { 22 | if len(fields) > 0 { 23 | return nil, func(s *sql.Selector) { 24 | BuildFieldSelect(s, fields) 25 | } 26 | } else { 27 | return nil, nil 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /entgo/query/select_test.go: -------------------------------------------------------------------------------- 1 | package entgo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "entgo.io/ent/dialect" 9 | "entgo.io/ent/dialect/sql" 10 | ) 11 | 12 | func TestBuildFieldSelect(t *testing.T) { 13 | t.Run("MySQL_2Fields", func(t *testing.T) { 14 | s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users")) 15 | 16 | BuildFieldSelect(s, []string{"id", "username"}) 17 | query, args := s.Query() 18 | require.Equal(t, "SELECT `id`, `username` FROM `users`", query) 19 | require.Empty(t, args) 20 | 21 | }) 22 | t.Run("PostgreSQL_2Fields", func(t *testing.T) { 23 | s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users")) 24 | 25 | BuildFieldSelect(s, []string{"id", "username"}) 26 | query, args := s.Query() 27 | require.Equal(t, `SELECT "id", "username" FROM "users"`, query) 28 | require.Empty(t, args) 29 | }) 30 | 31 | t.Run("MySQL_AllFields", func(t *testing.T) { 32 | s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users")) 33 | 34 | BuildFieldSelect(s, []string{}) 35 | query, args := s.Query() 36 | require.Equal(t, "SELECT * FROM `users`", query) 37 | require.Empty(t, args) 38 | 39 | }) 40 | t.Run("PostgreSQL_AllFields", func(t *testing.T) { 41 | s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users")) 42 | 43 | BuildFieldSelect(s, []string{}) 44 | query, args := s.Query() 45 | require.Equal(t, `SELECT * FROM "users"`, query) 46 | require.Empty(t, args) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /entgo/update/update.go: -------------------------------------------------------------------------------- 1 | package entgo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "entgo.io/ent/dialect/sql" 8 | 9 | "github.com/tx7do/go-utils/fieldmaskutil" 10 | "github.com/tx7do/go-utils/stringcase" 11 | 12 | "google.golang.org/protobuf/proto" 13 | "google.golang.org/protobuf/reflect/protoreflect" 14 | ) 15 | 16 | func BuildSetNullUpdate(u *sql.UpdateBuilder, fields []string) { 17 | if len(fields) > 0 { 18 | for _, field := range fields { 19 | field = stringcase.ToSnakeCase(field) 20 | u.SetNull(field) 21 | } 22 | } 23 | } 24 | 25 | // BuildSetNullUpdater 构建一个UpdateBuilder,用于清空字段的值 26 | func BuildSetNullUpdater(fields []string) func(u *sql.UpdateBuilder) { 27 | if len(fields) == 0 { 28 | return nil 29 | } 30 | 31 | return func(u *sql.UpdateBuilder) { 32 | BuildSetNullUpdate(u, fields) 33 | } 34 | } 35 | 36 | // ExtractJsonFieldKeyValues 提取json字段的键值对 37 | func ExtractJsonFieldKeyValues(msg proto.Message, paths []string, needToSnakeCase bool) []string { 38 | var keyValues []string 39 | rft := msg.ProtoReflect() 40 | for _, path := range paths { 41 | fd := rft.Descriptor().Fields().ByName(protoreflect.Name(path)) 42 | if fd == nil { 43 | continue 44 | } 45 | if !rft.Has(fd) { 46 | continue 47 | } 48 | 49 | var k string 50 | if needToSnakeCase { 51 | k = stringcase.ToSnakeCase(path) 52 | } else { 53 | k = path 54 | } 55 | 56 | keyValues = append(keyValues, fmt.Sprintf("'%s'", k)) 57 | 58 | v := rft.Get(fd) 59 | switch v.Interface().(type) { 60 | case int32, int64, uint32, uint64, float32, float64, bool: 61 | keyValues = append(keyValues, fmt.Sprintf("%d", v.Interface())) 62 | case string: 63 | keyValues = append(keyValues, fmt.Sprintf("'%s'", v.Interface())) 64 | } 65 | } 66 | 67 | return keyValues 68 | } 69 | 70 | // SetJsonNullFieldUpdateBuilder 设置json字段的空值 71 | func SetJsonNullFieldUpdateBuilder(fieldName string, msg proto.Message, paths []string) func(u *sql.UpdateBuilder) { 72 | nilPaths := fieldmaskutil.NilValuePaths(msg, paths) 73 | if len(nilPaths) == 0 { 74 | return nil 75 | } 76 | 77 | return func(u *sql.UpdateBuilder) { 78 | u.Set(fieldName, 79 | sql.Expr( 80 | fmt.Sprintf("\"%s\" - '{%s}'::text[]", fieldName, strings.Join(nilPaths, ",")), 81 | ), 82 | ) 83 | } 84 | } 85 | 86 | // SetJsonFieldValueUpdateBuilder 设置json字段的值 87 | func SetJsonFieldValueUpdateBuilder(fieldName string, msg proto.Message, paths []string, needToSnakeCase bool) func(u *sql.UpdateBuilder) { 88 | keyValues := ExtractJsonFieldKeyValues(msg, paths, needToSnakeCase) 89 | if len(keyValues) == 0 { 90 | return nil 91 | } 92 | 93 | return func(u *sql.UpdateBuilder) { 94 | u.Set(fieldName, 95 | sql.Expr( 96 | fmt.Sprintf("\"%s\" || jsonb_build_object(%s)", fieldName, strings.Join(keyValues, ",")), 97 | ), 98 | ) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /entgo/update/update_test.go: -------------------------------------------------------------------------------- 1 | package entgo 2 | 3 | import ( 4 | "testing" 5 | 6 | "entgo.io/ent/dialect" 7 | "entgo.io/ent/dialect/sql" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBuildSetNullUpdate(t *testing.T) { 13 | t.Run("MySQL_Set2", func(t *testing.T) { 14 | s := sql.Dialect(dialect.MySQL).Update("users") 15 | 16 | BuildSetNullUpdate(s, []string{"id", "username"}) 17 | query, args := s.Query() 18 | require.Equal(t, "UPDATE `users` SET `id` = NULL, `username` = NULL", query) 19 | require.Empty(t, args) 20 | 21 | }) 22 | t.Run("PostgreSQL_Set2", func(t *testing.T) { 23 | s := sql.Dialect(dialect.Postgres).Update("users") 24 | 25 | BuildSetNullUpdate(s, []string{"id", "username"}) 26 | query, args := s.Query() 27 | require.Equal(t, `UPDATE "users" SET "id" = NULL, "username" = NULL`, query) 28 | require.Empty(t, args) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /fieldmaskutil/fieldmaskutil_test.go: -------------------------------------------------------------------------------- 1 | package fieldmaskutil 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_NestedMaskFromPaths(t *testing.T) { 9 | type args struct { 10 | paths []string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want NestedMask 16 | }{ 17 | { 18 | name: "no nested fields", 19 | args: args{paths: []string{"a", "b", "c"}}, 20 | want: NestedMask{"a": NestedMask{}, "b": NestedMask{}, "c": NestedMask{}}, 21 | }, 22 | { 23 | name: "with nested fields", 24 | args: args{paths: []string{"aaa.bb.c", "dd.e", "f"}}, 25 | want: NestedMask{ 26 | "aaa": NestedMask{"bb": NestedMask{"c": NestedMask{}}}, 27 | "dd": NestedMask{"e": NestedMask{}}, 28 | "f": NestedMask{}}, 29 | }, 30 | { 31 | name: "single field", 32 | args: args{paths: []string{"a"}}, 33 | want: NestedMask{"a": NestedMask{}}, 34 | }, 35 | { 36 | name: "empty fields", 37 | args: args{paths: []string{}}, 38 | want: NestedMask{}, 39 | }, 40 | { 41 | name: "invalid input", 42 | args: args{paths: []string{".", "..", "..."}}, 43 | want: NestedMask{}, 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | if got := NestedMaskFromPaths(tt.args.paths); !reflect.DeepEqual(got, tt.want) { 49 | t.Errorf("NestedMaskFromPaths() = %v, want %v", got, tt.want) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | func BenchmarkNestedMaskFromPaths(b *testing.B) { 56 | for i := 0; i < b.N; i++ { 57 | NestedMaskFromPaths([]string{"aaa.bbb.c.d.e.f", "aa.b.cc.ddddddd", "e", "f", "g.h.i.j.k"}) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /geoip/README.md: -------------------------------------------------------------------------------- 1 | # IP地理位置查询 2 | 3 | ## 支持库 4 | 5 | - [GeoLite2](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data) 6 | - [纯真](https://update.cz88.net/geo-public) 7 | -------------------------------------------------------------------------------- /geoip/geoip.go: -------------------------------------------------------------------------------- 1 | package geoip 2 | 3 | // Result 归属地信息 4 | type Result struct { 5 | IP string `json:"ip"` 6 | Country string `json:"country"` // 国家 7 | Province string `json:"province"` // 省 8 | City string `json:"city"` // 城市 9 | ISP string `json:"isp"` // 服务提供商 10 | } 11 | 12 | // GeoIP 客户端 13 | type GeoIP interface { 14 | Query(queryIp string) (res Result, err error) 15 | } 16 | -------------------------------------------------------------------------------- /geoip/geoip_test.go: -------------------------------------------------------------------------------- 1 | package geoip 2 | -------------------------------------------------------------------------------- /geoip/geolite/README.md: -------------------------------------------------------------------------------- 1 | # GEOIP2(GeoLite2) 2 | 3 | GeoLite2数据库是免费的IP地理位置数据库,它可以通过IP地址识别其所在的城市和国家。与MaxMind的GeoIP2数据库相比,准确性较差。 4 | 5 | GeoLite2-*.mmdb文件是一个二进制数据库文件,包含了MaxMind GeoLite2所需的全部数据。 6 | 7 | GeoLite2的数据库分为:国家(Country),城市(City)和ASN三个数据库。每周两次更新。 8 | 9 | ## 数据库下载地址 10 | 11 | 1. 12 | 2. 13 | -------------------------------------------------------------------------------- /geoip/geolite/assets/GeoLite2-City.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tx7do/go-utils/b2efe51e40a5acc2e89522ac34975439a3ffda33/geoip/geolite/assets/GeoLite2-City.mmdb -------------------------------------------------------------------------------- /geoip/geolite/assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import _ "embed" 4 | 5 | //go:embed GeoLite2-City.mmdb 6 | var GeoLite2CityData []byte 7 | -------------------------------------------------------------------------------- /geoip/geolite/geolite.go: -------------------------------------------------------------------------------- 1 | package geolite 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | 7 | "github.com/go-kratos/kratos/v2/log" 8 | "github.com/oschwald/geoip2-golang" 9 | 10 | "github.com/tx7do/go-utils/geoip" 11 | "github.com/tx7do/go-utils/geoip/geolite/assets" 12 | ) 13 | 14 | const defaultOutputLanguage = "zh-CN" 15 | 16 | // Client 地理位置解析结构体 17 | type Client struct { 18 | db *geoip2.Reader 19 | outputLanguage string 20 | } 21 | 22 | // NewClient . 23 | func NewClient() (*Client, error) { 24 | db, err := geoip2.FromBytes(assets.GeoLite2CityData) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return &Client{db: db, outputLanguage: defaultOutputLanguage}, nil 29 | } 30 | 31 | // Close 关闭客户端 32 | func (g *Client) Close() error { 33 | if g.db == nil { 34 | return nil 35 | } 36 | return g.db.Close() 37 | } 38 | 39 | // SetLanguage 设置输出的语言,默认为:zh-CN 40 | func (g *Client) SetLanguage(code string) { 41 | g.outputLanguage = code 42 | } 43 | 44 | // query 查询城市级别数据 45 | func (g *Client) query(rawIP string) (city *geoip2.City, err error) { 46 | ip := net.ParseIP(rawIP) 47 | if ip == nil { 48 | return nil, errors.New("invalid ip") 49 | } 50 | 51 | return g.db.City(ip) 52 | } 53 | 54 | // Query 通过IP获取地区 55 | func (g *Client) Query(rawIP string) (ret geoip.Result, err error) { 56 | record, err := g.query(rawIP) 57 | if err != nil { 58 | log.Fatal(err) 59 | return ret, err 60 | } 61 | 62 | ret.Country = record.Country.Names[g.outputLanguage] 63 | if len(record.Subdivisions) > 0 { 64 | ret.Province = record.Subdivisions[0].Names[g.outputLanguage] 65 | } 66 | ret.City = record.City.Names[g.outputLanguage] 67 | 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /geoip/geolite/geolite_test.go: -------------------------------------------------------------------------------- 1 | package geolite 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestGeoLite(t *testing.T) { 9 | g, err := NewClient() 10 | assert.Nil(t, err) 11 | 12 | ret, err := g.Query("47.108.149.89") 13 | assert.Nil(t, err) 14 | assert.Equal(t, ret.Country, "中国") 15 | assert.Equal(t, ret.Province, "四川省") 16 | assert.Equal(t, ret.City, "成都") 17 | } 18 | -------------------------------------------------------------------------------- /geoip/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tx7do/go-utils/geoip 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/go-kratos/kratos/v2 v2.8.4 9 | github.com/oschwald/geoip2-golang v1.11.0 10 | github.com/stretchr/testify v1.10.0 11 | golang.org/x/text v0.25.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/kr/text v0.2.0 // indirect 17 | github.com/oschwald/maxminddb-golang v1.13.1 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | golang.org/x/sys v0.33.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | 23 | replace github.com/tx7do/go-utils => ../ 24 | -------------------------------------------------------------------------------- /geoip/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-kratos/kratos/v2 v2.8.4 h1:eIJLE9Qq9WSoKx+Buy2uPyrahtF/lPh+Xf4MTpxhmjs= 5 | github.com/go-kratos/kratos/v2 v2.8.4/go.mod h1:mq62W2101a5uYyRxe+7IdWubu7gZCGYqSNKwGFiiRcw= 6 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 7 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 8 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 9 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 10 | github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w= 11 | github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= 12 | github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= 13 | github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 17 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 18 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 19 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 20 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 21 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 22 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 23 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 24 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 25 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 28 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 29 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 30 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | -------------------------------------------------------------------------------- /geoip/qqwry/README.md: -------------------------------------------------------------------------------- 1 | # QQWry 2 | 3 | ## 数据库下载地址 4 | 5 | - [纯真社区版IP库](https://update.cz88.net/geo-public) 6 | - [qqwry.dat - out0fmemory - Github](https://github.com/out0fmemory/qqwry.dat/blob/master/qqwry_lastest.dat) 7 | - [qqwry - xiaoqidun - Github](https://github.com/xiaoqidun/qqwry) 8 | - [qqwry - freshcn - Github](https://github.com/freshcn/qqwry) 9 | -------------------------------------------------------------------------------- /geoip/qqwry/assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import _ "embed" 4 | 5 | //go:embed qqwry.dat 6 | var QQWryDat []byte 7 | -------------------------------------------------------------------------------- /geoip/qqwry/assets/qqwry.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tx7do/go-utils/b2efe51e40a5acc2e89522ac34975439a3ffda33/geoip/qqwry/assets/qqwry.dat -------------------------------------------------------------------------------- /geoip/qqwry/consts.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | const ( 4 | ipRecordLength = 7 // IndexLen 索引长度 5 | 6 | redirectMode1 = 0x01 // RedirectMode1 国家的类型, 指向另一个指向 7 | 8 | redirectMode2 = 0x02 // RedirectMode2 国家的类型, 指向一个指向 9 | ) 10 | -------------------------------------------------------------------------------- /geoip/qqwry/qqwry.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "github.com/tx7do/go-utils/geoip" 7 | "net" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/tx7do/go-utils/geoip/qqwry/assets" 12 | ) 13 | 14 | type Client struct { 15 | data []byte 16 | dataLen uint32 17 | ipCache sync.Map 18 | 19 | IPNum int64 20 | 21 | startPos uint32 22 | endPos uint32 23 | } 24 | 25 | func NewClient() *Client { 26 | cli := &Client{ 27 | ipCache: sync.Map{}, 28 | } 29 | 30 | cli.init() 31 | 32 | return cli 33 | } 34 | 35 | func (c *Client) init() { 36 | c.startPos, c.endPos = c.readHeader() 37 | c.IPNum = int64((c.endPos-c.startPos)/ipRecordLength + 1) 38 | } 39 | 40 | // parseIp 解析IP 41 | func (c *Client) parseIp(queryIp string) (uint32, error) { 42 | ip := net.ParseIP(queryIp).To4() 43 | if ip == nil { 44 | return 0, errors.New("ip is not ipv4") 45 | } 46 | ip32 := binary.BigEndian.Uint32(ip) 47 | return ip32, nil 48 | } 49 | 50 | // readHeader 读取文件头 51 | func (c *Client) readHeader() (uint32, uint32) { 52 | startPos := binary.LittleEndian.Uint32(assets.QQWryDat[:4]) 53 | endPos := binary.LittleEndian.Uint32(assets.QQWryDat[4:8]) 54 | return startPos, endPos 55 | } 56 | 57 | // readMode 获取偏移值类型 58 | func (c *Client) readMode(offset uint32) byte { 59 | return assets.QQWryDat[offset] 60 | } 61 | 62 | // readIpRecord 读取IP记录 前4字节:起始IP,后3字节:偏移量 63 | func (c *Client) readIpRecord(offset uint32) (ip32 uint32, ipOffset uint32) { 64 | buf := assets.QQWryDat[offset : offset+ipRecordLength] 65 | ip32 = binary.LittleEndian.Uint32(buf[:4]) 66 | ipOffset = byte3ToUInt32(buf[4:]) 67 | return ip32, ipOffset 68 | } 69 | 70 | // locateIP 定位IP 71 | func (c *Client) locateIP(ip32 uint32) int32 { 72 | var _ip32 uint32 73 | var _ipOffset uint32 74 | var offset uint32 75 | 76 | var mid uint32 77 | i := c.startPos 78 | j := c.endPos 79 | for { 80 | mid = getMiddleOffset(i, j) 81 | _ip32, _ipOffset = c.readIpRecord(mid) 82 | 83 | if j-i == ipRecordLength { 84 | offset = _ipOffset 85 | _ip32, _ipOffset = c.readIpRecord(mid + ipRecordLength) 86 | if ip32 < _ip32 { 87 | break 88 | } else { 89 | offset = 0 90 | break 91 | } 92 | } 93 | 94 | if _ip32 > ip32 { 95 | j = mid 96 | } else if _ip32 < ip32 { 97 | i = mid 98 | } else if _ip32 == ip32 { 99 | offset = _ipOffset 100 | break 101 | } 102 | } 103 | 104 | return int32(offset) 105 | } 106 | 107 | // readArea 读取区域 108 | func (c *Client) readArea(offset uint32) []byte { 109 | mode := c.readMode(offset) 110 | if mode == redirectMode1 || mode == redirectMode2 { 111 | areaOffset := c.readUInt24(int32(offset) + 1) 112 | if areaOffset == 0 { 113 | return []byte{} 114 | } 115 | return c.readString(areaOffset) 116 | } 117 | 118 | return c.readString(offset) 119 | } 120 | 121 | // readString 获取字符串 122 | func (c *Client) readString(offset uint32) []byte { 123 | data := make([]byte, 0, 30) 124 | for i := offset; i < uint32(len(assets.QQWryDat)); i++ { 125 | if assets.QQWryDat[i] == 0 { 126 | data = assets.QQWryDat[offset:i] 127 | break 128 | } 129 | } 130 | return data 131 | } 132 | 133 | func (c *Client) readUInt24(offset int32) uint32 { 134 | i := uint32(assets.QQWryDat[offset+0]) & 0xFF 135 | i |= (uint32(assets.QQWryDat[offset+1]) << 8) & 0xFF00 136 | i |= (uint32(assets.QQWryDat[offset+2]) << 16) & 0xFF0000 137 | return i 138 | } 139 | 140 | func (c *Client) Query(queryIp string) (res geoip.Result, err error) { 141 | res.IP = queryIp 142 | res.Country = "中国" 143 | 144 | ip32, err := c.parseIp(queryIp) 145 | if err != nil { 146 | return 147 | } 148 | 149 | offset := c.locateIP(ip32) 150 | if offset <= 0 { 151 | err = errors.New("ip not found") 152 | return 153 | } 154 | 155 | //读取第一个字节判断是否是标志字节 156 | offset += 4 157 | mode := c.readMode(uint32(offset)) 158 | 159 | var _area []byte 160 | var area string 161 | 162 | var ispPos uint32 163 | switch mode { 164 | case redirectMode1: 165 | posC := c.readUInt24(offset + 1) 166 | mode = c.readMode(posC) 167 | posCA := posC 168 | if mode == redirectMode2 { 169 | posCA = c.readUInt24(int32(posC) + 1) 170 | posC += 4 171 | } 172 | _area = c.readString(posCA) 173 | if mode != redirectMode2 { 174 | posC += uint32(len(area) + 1) 175 | } 176 | ispPos = posC 177 | 178 | case redirectMode2: 179 | posCA := c.readUInt24(offset + 1) 180 | _area = c.readString(posCA) 181 | ispPos = uint32(offset) + 4 182 | 183 | default: 184 | posCA := offset + 0 185 | _area = c.readString(uint32(posCA)) 186 | ispPos = uint32(offset) + uint32(len(area)) + 1 187 | } 188 | 189 | if len(_area) != 0 { 190 | area = strings.TrimSpace(gb18030Decode(_area)) 191 | 192 | areas := SpiltAddress(area) 193 | if len(areas) == 2 { 194 | res.Province = areas[0] 195 | res.City = areas[1] 196 | } else if len(areas) == 1 { 197 | res.City = areas[0] 198 | } else { 199 | res.City = area 200 | } 201 | } 202 | 203 | ispMode := assets.QQWryDat[ispPos] 204 | if ispMode == redirectMode1 || ispMode == redirectMode2 { 205 | ispPos = c.readUInt24(int32(ispPos + 1)) 206 | } 207 | if ispPos > 0 { 208 | var _isp []byte 209 | _isp = c.readString(ispPos) 210 | res.ISP = strings.TrimSpace(gb18030Decode(_isp)) 211 | if res.ISP != "" { 212 | if strings.Contains(res.ISP, "CZ88.NET") { 213 | res.ISP = "" 214 | } 215 | } 216 | } 217 | 218 | return 219 | } 220 | -------------------------------------------------------------------------------- /geoip/qqwry/qqwry_test.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestClient(t *testing.T) { 11 | g := NewClient() 12 | assert.NotNil(t, g) 13 | 14 | res, err := g.Query("47.108.149.89") 15 | assert.Nil(t, err) 16 | fmt.Println("国家:", res.Country, "省:", res.Province, "城市:", res.City, "服务商:", res.ISP) 17 | assert.Equal(t, res.Country, "中国") 18 | assert.Equal(t, res.Province, "四川省") 19 | assert.Equal(t, res.City, "成都市") 20 | assert.Equal(t, res.ISP, "阿里云") 21 | } 22 | -------------------------------------------------------------------------------- /geoip/qqwry/utils.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "regexp" 7 | 8 | "golang.org/x/text/encoding/simplifiedchinese" 9 | "golang.org/x/text/transform" 10 | ) 11 | 12 | var regSpiltAddress = regexp.MustCompile(`.+?(省|市|自治区|自治州|盟|县|区|管委会|街道|镇|乡)`) 13 | 14 | func gb18030Decode(src []byte) string { 15 | in := bytes.NewReader(src) 16 | out := transform.NewReader(in, simplifiedchinese.GB18030.NewDecoder()) 17 | d, _ := io.ReadAll(out) 18 | return string(d) 19 | } 20 | 21 | // getMiddleOffset 取得begin和end之间的偏移量,用于二分搜索 22 | func getMiddleOffset(start uint32, end uint32) uint32 { 23 | return start + (((end-start)/ipRecordLength)>>1)*ipRecordLength 24 | } 25 | 26 | func byte3ToUInt32(data []byte) uint32 { 27 | i := uint32(data[0]) & 0xff 28 | i |= (uint32(data[1]) << 8) & 0xff00 29 | i |= (uint32(data[2]) << 16) & 0xff0000 30 | return i 31 | } 32 | 33 | func SpiltAddress(addr string) []string { 34 | return regSpiltAddress.FindAllString(addr, -1) 35 | } 36 | -------------------------------------------------------------------------------- /geoip/qqwry/utils_test.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestSpiltAddress(t *testing.T) { 9 | names := SpiltAddress("浙江省杭州市西湖区") 10 | fmt.Println(names) 11 | } 12 | -------------------------------------------------------------------------------- /gitautotag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CURTAG=`git describe --abbrev=0 --tags`; 4 | CURTAG="${CURTAG/v/}" 5 | 6 | IFS='.' read -a vers <<< "$CURTAG" 7 | 8 | MAJ=${vers[0]} 9 | MIN=${vers[1]} 10 | BUG=${vers[2]} 11 | echo "Current Tag: v$MAJ.$MIN.$BUG" 12 | 13 | for cmd in "$@" 14 | do 15 | case $cmd in 16 | "--major") 17 | # $((MAJ+1)) 18 | ((MAJ+=1)) 19 | MIN=0 20 | BUG=0 21 | echo "Incrementing Major Version#" 22 | ;; 23 | "--minor") 24 | ((MIN+=1)) 25 | BUG=0 26 | echo "Incrementing Minor Version#" 27 | ;; 28 | "--bug") 29 | ((BUG+=1)) 30 | echo "Incrementing Bug Version#" 31 | ;; 32 | esac 33 | done 34 | NEWTAG="v$MAJ.$MIN.$BUG" 35 | echo "Adding Tag: $NEWTAG"; 36 | git tag -a $NEWTAG -m $NEWTAG 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tx7do/go-utils 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/gobwas/glob v0.2.3 9 | github.com/google/uuid v1.6.0 10 | github.com/stretchr/testify v1.10.0 11 | golang.org/x/crypto v0.38.0 12 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 13 | google.golang.org/protobuf v1.36.6 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/kr/pretty v0.3.1 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | github.com/rogpeppe/go-internal v1.10.0 // indirect 21 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 5 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 6 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 7 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 9 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 10 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 11 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 12 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 13 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 14 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 15 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 16 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 17 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 21 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 22 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 23 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 24 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 25 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 26 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 27 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= 28 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= 29 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 30 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 33 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /gorm/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tx7do/go-utils/gorm 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | gorm.io/driver/clickhouse v0.6.1 9 | gorm.io/driver/mysql v1.5.7 10 | gorm.io/driver/postgres v1.5.11 11 | gorm.io/driver/sqlite v1.5.7 12 | gorm.io/driver/sqlserver v1.5.4 13 | gorm.io/gorm v1.26.1 14 | gorm.io/plugin/opentelemetry v0.1.14 15 | ) 16 | 17 | require ( 18 | filippo.io/edwards25519 v1.1.0 // indirect 19 | github.com/ClickHouse/ch-go v0.66.0 // indirect 20 | github.com/ClickHouse/clickhouse-go/v2 v2.34.0 // indirect 21 | github.com/andybalholm/brotli v1.1.1 // indirect 22 | github.com/go-faster/city v1.0.1 // indirect 23 | github.com/go-faster/errors v0.7.1 // indirect 24 | github.com/go-logr/logr v1.4.2 // indirect 25 | github.com/go-logr/stdr v1.2.2 // indirect 26 | github.com/go-sql-driver/mysql v1.9.2 // indirect 27 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 28 | github.com/golang-sql/sqlexp v0.1.0 // indirect 29 | github.com/google/uuid v1.6.0 // indirect 30 | github.com/hashicorp/go-version v1.7.0 // indirect 31 | github.com/jackc/pgpassfile v1.0.0 // indirect 32 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 33 | github.com/jackc/pgx/v5 v5.7.5 // indirect 34 | github.com/jackc/puddle/v2 v2.2.2 // indirect 35 | github.com/jinzhu/inflection v1.0.0 // indirect 36 | github.com/jinzhu/now v1.1.5 // indirect 37 | github.com/klauspost/compress v1.18.0 // indirect 38 | github.com/mattn/go-sqlite3 v1.14.28 // indirect 39 | github.com/microsoft/go-mssqldb v1.8.1 // indirect 40 | github.com/paulmach/orb v0.11.1 // indirect 41 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 42 | github.com/segmentio/asm v1.2.0 // indirect 43 | github.com/shopspring/decimal v1.4.0 // indirect 44 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 45 | go.opentelemetry.io/otel v1.36.0 // indirect 46 | go.opentelemetry.io/otel/metric v1.36.0 // indirect 47 | go.opentelemetry.io/otel/trace v1.36.0 // indirect 48 | golang.org/x/crypto v0.38.0 // indirect 49 | golang.org/x/sync v0.14.0 // indirect 50 | golang.org/x/sys v0.33.0 // indirect 51 | golang.org/x/text v0.25.0 // indirect 52 | gopkg.in/yaml.v3 v3.0.1 // indirect 53 | ) 54 | 55 | replace github.com/tx7do/go-utils => ../ 56 | -------------------------------------------------------------------------------- /gorm/gorm_client.go: -------------------------------------------------------------------------------- 1 | package gorm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gorm.io/gorm" 7 | 8 | "gorm.io/plugin/opentelemetry/tracing" 9 | 10 | "gorm.io/driver/clickhouse" 11 | "gorm.io/driver/mysql" 12 | "gorm.io/driver/postgres" 13 | "gorm.io/driver/sqlite" 14 | "gorm.io/driver/sqlserver" 15 | ) 16 | 17 | type Client struct { 18 | *gorm.DB 19 | 20 | err error 21 | } 22 | 23 | func NewClient(driverName, dsn string, enableMigrate, enableTrace, enableMetrics bool, gormCfg *gorm.Config) *Client { 24 | c := &Client{} 25 | 26 | if gormCfg == nil { 27 | gormCfg = &gorm.Config{} 28 | } 29 | 30 | c.err = c.createGormClient(driverName, dsn, enableMigrate, enableTrace, enableMetrics, gormCfg) 31 | 32 | return c 33 | } 34 | 35 | func (c *Client) Error() error { 36 | return c.err 37 | } 38 | 39 | // createGormClient 创建GORM的客户端 40 | func (c *Client) createGormClient(driverName, dsn string, enableMigrate, enableTrace, enableMetrics bool, gormCfg *gorm.Config) error { 41 | var driver gorm.Dialector 42 | switch driverName { 43 | default: 44 | fallthrough 45 | case "mysql": 46 | driver = mysql.Open(dsn) 47 | break 48 | case "postgres": 49 | driver = postgres.Open(dsn) 50 | break 51 | case "clickhouse": 52 | driver = clickhouse.Open(dsn) 53 | break 54 | case "sqlite": 55 | driver = sqlite.Open(dsn) 56 | break 57 | case "sqlserver": 58 | driver = sqlserver.Open(dsn) 59 | break 60 | } 61 | 62 | client, err := gorm.Open(driver, gormCfg) 63 | if err != nil { 64 | return fmt.Errorf("failed opening connection to db: %v", err) 65 | } 66 | 67 | if enableTrace { 68 | var opts []tracing.Option 69 | if enableMetrics { 70 | opts = append(opts, tracing.WithoutMetrics()) 71 | } 72 | 73 | if err = client.Use(tracing.NewPlugin(opts...)); err != nil { 74 | return fmt.Errorf("failed opening connection to db: %v", err) 75 | } 76 | } 77 | 78 | // 运行数据库迁移工具 79 | if enableMigrate { 80 | if err = client.AutoMigrate( 81 | getMigrateModels()..., 82 | ); err != nil { 83 | return fmt.Errorf("failed creating schema resources: %v", err) 84 | } 85 | } 86 | 87 | c.DB = client 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /gorm/migrates.go: -------------------------------------------------------------------------------- 1 | package gorm 2 | 3 | var migrateModels []interface{} 4 | 5 | // RegisterMigrateModel 注册用于数据库迁移的数据库模型 6 | func RegisterMigrateModel(model interface{}) { 7 | migrateModels = append(migrateModels, &model) 8 | } 9 | 10 | // getMigrateModels 获取用于数据库迁移的数据库模型 11 | func getMigrateModels() []interface{} { 12 | return migrateModels 13 | } 14 | -------------------------------------------------------------------------------- /id/README.md: -------------------------------------------------------------------------------- 1 | # ID生成器 2 | 3 | ## 订单ID 4 | 5 | - 电商平台:202506041234567890(时间戳 + 随机数,19-20 位)。 6 | - 支付系统:PAY20250604123456789(业务前缀 + 时间戳 + 序号)。 7 | - 微信支付:1589123456789012345(类似 Snowflake 的纯数字 ID)。 8 | - 美团订单:202506041234567890123(时间戳 + 商户 ID + 随机数)。 9 | 10 | ## UUID 11 | 12 | | 特性 | GUID/UUID | KSUID | ShortUUID | XID | Snowflake | 13 | |-------|--------------|------------|-----------|----------|----------------| 14 | | 长度 | 36/32字符(不含-) | 27字符 | 22字符 | 20字符 | 19(数字位数) | 15 | | 有序性 | 无序(UUIDv4) | 严格时序 | 无序 | 趋势有序 | 严格时序 | 16 | | 时间精度 | 无(UUIDv4) | 毫秒级 | 无 | 秒级 | 毫秒级 | 17 | | 分布式安全 | 高(随机数) | 高 | 高 | 高 | 高(需配置WorkerID) | 18 | | 性能 | 中等 | 中等 | 较低(编码开销) | 极高 | 极高 | 19 | | 时钟依赖 | 无 | 有(需处理时钟回拨) | 无 | 有(但影响较小) | 强依赖(需严格同步) | 20 | | 适用场景 | 跨系统兼容 | 时序索引 | 短ID、URL | 高并发、短ID | 分布式时序ID | 21 | 22 | ## 选择建议 23 | 24 | - **GUID/UUID**: 适用于需要跨系统兼容的场景,特别是当不需要有序性时。 25 | - **KSUID**: 适合需要严格时序的应用,如事件日志、时间序列数据。 26 | - **ShortUUID**: 当需要短ID且不关心有序性时的理想选择,适用于URL、短链接等。 27 | - **XID**: 高并发场景下的短ID选择,适合需要一定有序性的应用。 28 | - **Snowflake**: 适合分布式系统,特别是需要严格时序和高性能的场景,如大规模分布式应用。 29 | -------------------------------------------------------------------------------- /id/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tx7do/go-utils/id 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/bwmarrin/snowflake v0.3.0 9 | github.com/google/uuid v1.6.0 10 | github.com/lithammer/shortuuid/v4 v4.2.0 11 | github.com/rs/xid v1.6.0 12 | github.com/segmentio/ksuid v1.0.4 13 | github.com/stretchr/testify v1.10.0 14 | github.com/tx7do/go-utils v1.1.27 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/kr/text v0.2.0 // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | github.com/rogpeppe/go-internal v1.13.1 // indirect 22 | github.com/sony/sonyflake v1.2.1 // indirect 23 | go.mongodb.org/mongo-driver v1.17.3 // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | 27 | replace github.com/tx7do/go-utils => ../ 28 | -------------------------------------------------------------------------------- /id/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= 2 | github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 7 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 8 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 9 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c= 13 | github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 17 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 18 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 19 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 20 | github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= 21 | github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= 22 | github.com/sony/sonyflake v1.2.1 h1:Jzo4abS84qVNbYamXZdrZF1/6TzNJjEogRfXv7TsG48= 23 | github.com/sony/sonyflake v1.2.1/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= 24 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 25 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 26 | go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= 27 | go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 30 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 31 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /id/order_id.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/tx7do/go-utils/trans" 11 | ) 12 | 13 | type idCounter uint32 14 | 15 | func (c *idCounter) Increase() uint32 { 16 | cur := *c 17 | atomic.AddUint32((*uint32)(c), 1) 18 | atomic.CompareAndSwapUint32((*uint32)(c), 1000, 0) 19 | return uint32(cur) 20 | } 21 | 22 | var orderIdIndex idCounter 23 | 24 | // GenerateOrderIdWithRandom 生成20位订单号,前缀 + 时间戳 + 随机数 25 | func GenerateOrderIdWithRandom(prefix string, tm *time.Time) string { 26 | // 前缀 + 时间戳(14位) + 随机数(4位) 27 | 28 | if tm == nil { 29 | tm = trans.Time(time.Now()) 30 | } 31 | 32 | timestamp := tm.Format("20060102150405") 33 | 34 | randNum := rand.Intn(10000) // 生成0-9999之间的随机数 35 | 36 | return fmt.Sprintf("%s%s%d", prefix, timestamp, randNum) 37 | } 38 | 39 | // GenerateOrderIdWithIncreaseIndex 生成20位订单号,前缀+时间+自增长索引 40 | func GenerateOrderIdWithIncreaseIndex(prefix string, tm *time.Time) string { 41 | if tm == nil { 42 | tm = trans.Time(time.Now()) 43 | } 44 | 45 | timestamp := tm.Format("20060102150405") 46 | 47 | index := orderIdIndex.Increase() 48 | 49 | return fmt.Sprintf("%s%s%d", prefix, timestamp, index) 50 | } 51 | 52 | // GenerateOrderIdWithTenantId 带商户ID的订单ID生成器:202506041234567890123 53 | func GenerateOrderIdWithTenantId(tenantID string) string { 54 | // 时间戳(14位) + 商户ID(固定 5 位) + 随机数(4位) 55 | 56 | // 时间戳部分(精确到毫秒) 57 | now := time.Now() 58 | timestamp := now.Format("20060102150405") 59 | 60 | // 商户ID部分(截取或补零到5位) 61 | tenantPart := tenantID 62 | if len(tenantPart) > 5 { 63 | tenantPart = tenantPart[:5] 64 | } else { 65 | tenantPart = fmt.Sprintf("%-5s", tenantPart) 66 | tenantPart = strings.ReplaceAll(tenantPart, " ", "0") 67 | } 68 | 69 | // 随机数部分(4位) 70 | n := rand.Int31n(10000) 71 | randomPart := fmt.Sprintf("%04d", n) 72 | 73 | return timestamp + tenantPart + randomPart 74 | } 75 | 76 | func GenerateOrderIdWithPrefixSonyflake(prefix string) string { 77 | id, _ := NewSonyflakeID() 78 | return fmt.Sprintf("%s%d", prefix, id) 79 | } 80 | 81 | func GenerateOrderIdWithPrefixSnowflake(workerId int64, prefix string) string { 82 | id, _ := NewSnowflakeID(workerId) 83 | return fmt.Sprintf("%s%d", prefix, id) 84 | } 85 | -------------------------------------------------------------------------------- /id/order_id_test.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGenerateOrderIdWithRandom(t *testing.T) { 13 | prefix := "PT" 14 | 15 | // 测试生成的订单号是否包含前缀 16 | orderID := GenerateOrderIdWithRandom(prefix, nil) 17 | assert.Contains(t, orderID, prefix, "订单号应包含前缀") 18 | t.Logf("GenerateOrderIdWithRandom: %s", orderID) 19 | 20 | // 测试生成的订单号长度是否正确 21 | assert.Equal(t, len(prefix)+14+4, len(orderID), "订单号长度应为前缀+时间戳+随机数") 22 | } 23 | 24 | func TestGenerateOrderIdWithIndex(t *testing.T) { 25 | prefix := "PT" 26 | 27 | tm := time.Now() 28 | 29 | fmt.Println(GenerateOrderIdWithIncreaseIndex(prefix, &(tm))) 30 | 31 | ids := make(map[string]bool) 32 | count := 100 33 | for i := 0; i < count; i++ { 34 | ids[GenerateOrderIdWithIncreaseIndex(prefix, &(tm))] = true 35 | } 36 | assert.Equal(t, count, len(ids)) 37 | } 38 | 39 | func TestGenerateOrderIdWithIndexThread(t *testing.T) { 40 | tm := time.Now() 41 | 42 | var wg sync.WaitGroup 43 | var ids sync.Map 44 | for i := 0; i < 10; i++ { 45 | wg.Add(1) 46 | go func() { 47 | for i := 0; i < 100; i++ { 48 | id := GenerateOrderIdWithIncreaseIndex("PT", &(tm)) 49 | ids.Store(id, true) 50 | } 51 | wg.Done() 52 | }() 53 | } 54 | wg.Wait() 55 | 56 | aLen := 0 57 | ids.Range(func(k, v interface{}) bool { 58 | aLen++ 59 | return true 60 | }) 61 | assert.Equal(t, 1000, aLen) 62 | } 63 | 64 | func TestGenerateOrderIdWithTenantId(t *testing.T) { 65 | tenantID := "M9876" 66 | orderID := GenerateOrderIdWithTenantId(tenantID) 67 | 68 | t.Logf(orderID) 69 | 70 | // 验证订单号长度是否正确 71 | assert.Equal(t, 14+5+4, len(orderID)) 72 | 73 | // 验证时间戳部分是否正确 74 | timestamp := time.Now().Format("20060102150405") 75 | assert.Contains(t, orderID, timestamp) 76 | t.Logf("timestamp %d", len(timestamp)) 77 | 78 | // 验证商户ID部分是否正确 79 | assert.Contains(t, orderID, tenantID) 80 | 81 | // 验证随机数部分是否为4位数字 82 | randomPart := orderID[len(orderID)-4:] 83 | assert.Regexp(t, `^\d{4}$`, randomPart) 84 | } 85 | 86 | func TestGenerateOrderIdWithTenantIdCollision(t *testing.T) { 87 | tenantID := "M9876" 88 | count := 1000 // 生成订单号的数量 89 | ids := make(map[string]bool) 90 | 91 | for i := 0; i < count; i++ { 92 | orderID := GenerateOrderIdWithTenantId(tenantID) 93 | if ids[orderID] { 94 | t.Errorf("碰撞的订单号: %s", orderID) 95 | } 96 | ids[orderID] = true 97 | } 98 | 99 | t.Logf("生成了 %d 个订单号,没有发生碰撞", count) 100 | } 101 | 102 | func TestGenerateOrderIdWithPrefixSonyflake(t *testing.T) { 103 | prefix := "ORD" 104 | orderID := GenerateOrderIdWithPrefixSonyflake(prefix) 105 | t.Logf("order id with prefix sonyflake: %s [%d]", orderID, len(orderID)) 106 | 107 | // 验证订单号是否包含前缀 108 | assert.Contains(t, orderID, prefix, "订单号应包含前缀") 109 | 110 | // 验证订单号是否为有效的数字字符串 111 | assert.Regexp(t, `^ORD\d+$`, orderID, "订单号格式应为前缀加数字") 112 | } 113 | 114 | func TestGenerateOrderIdWithPrefixSonyflakeCollision(t *testing.T) { 115 | prefix := "ORD" 116 | count := 100000 // 生成订单号的数量 117 | ids := make(map[string]bool) 118 | 119 | for i := 0; i < count; i++ { 120 | orderID := GenerateOrderIdWithPrefixSonyflake(prefix) 121 | if ids[orderID] { 122 | t.Errorf("碰撞的订单号: %s", orderID) 123 | } 124 | ids[orderID] = true 125 | } 126 | 127 | t.Logf("生成了 %d 个订单号,没有发生碰撞", count) 128 | } 129 | 130 | func TestGenerateOrderIdWithPrefixSnowflake(t *testing.T) { 131 | workerId := int64(1) // 假设使用的 workerId 132 | prefix := "ORD" 133 | orderID := GenerateOrderIdWithPrefixSnowflake(workerId, prefix) 134 | t.Logf("order id with prefix snowflake: %s [%d]", orderID, len(orderID)) 135 | 136 | // 验证订单号是否包含前缀 137 | assert.Contains(t, orderID, prefix, "订单号应包含前缀") 138 | 139 | // 验证订单号是否为有效的数字字符串 140 | assert.Regexp(t, `^ORD\d+$`, orderID, "订单号格式应为前缀加数字") 141 | } 142 | 143 | func TestGenerateOrderIdWithPrefixSnowflakeCollision(t *testing.T) { 144 | workerId := int64(1) // 假设使用的 workerId 145 | prefix := "ORD" 146 | count := 1000000 // 生成订单号的数量 147 | ids := make(map[string]bool) 148 | 149 | for i := 0; i < count; i++ { 150 | orderID := GenerateOrderIdWithPrefixSnowflake(workerId, prefix) 151 | if ids[orderID] { 152 | t.Errorf("碰撞的订单号: %s", orderID) 153 | } 154 | ids[orderID] = true 155 | } 156 | 157 | t.Logf("生成了 %d 个订单号,没有发生碰撞", count) 158 | } 159 | -------------------------------------------------------------------------------- /id/snowflake.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/bwmarrin/snowflake" 8 | ) 9 | 10 | var snowflakeNodeMap = sync.Map{} 11 | 12 | type SnowflakeNode struct { 13 | workerId int64 14 | node *snowflake.Node 15 | sync.Mutex 16 | } 17 | 18 | func NewSnowflakeNode(workerId int64) (*SnowflakeNode, error) { 19 | node, err := snowflake.NewNode(workerId) 20 | return &SnowflakeNode{ 21 | workerId: workerId, 22 | node: node, 23 | Mutex: sync.Mutex{}, 24 | }, err 25 | } 26 | 27 | func (sfNode *SnowflakeNode) Generate() int64 { 28 | sfNode.Lock() 29 | defer sfNode.Unlock() 30 | return sfNode.node.Generate().Int64() 31 | } 32 | 33 | func (sfNode *SnowflakeNode) GenerateString() string { 34 | sfNode.Lock() 35 | defer sfNode.Unlock() 36 | return sfNode.node.Generate().String() 37 | } 38 | 39 | func NewSnowflakeID(workerId int64) (int64, error) { 40 | // 64 位 ID = 41 位时间戳 + 10 位工作节点 ID + 12 位序列号 41 | 42 | var node *SnowflakeNode 43 | var err error 44 | find, ok := snowflakeNodeMap.Load(workerId) 45 | if ok { 46 | node = find.(*SnowflakeNode) 47 | } else { 48 | node, err = NewSnowflakeNode(workerId) 49 | if err != nil { 50 | //log.Println(err) 51 | return 0, err 52 | } 53 | snowflakeNodeMap.Store(workerId, node) 54 | } 55 | if node == nil { 56 | //log.Println("snowflake node is nil") 57 | return 0, errors.New("snowflake node is nil") 58 | } 59 | 60 | return node.Generate(), nil 61 | } 62 | 63 | func GenerateSnowflakeID(workerId int64) int64 { 64 | id, _ := NewSnowflakeID(workerId) 65 | return id 66 | } 67 | -------------------------------------------------------------------------------- /id/sonyflake.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/sony/sonyflake" 7 | ) 8 | 9 | var ( 10 | sf *sonyflake.Sonyflake 11 | sfMu sync.Mutex 12 | ) 13 | 14 | func NewSonyflakeID() (uint64, error) { 15 | // 64 位 ID = 39 位时间戳 + 8 位机器 ID + 16 位序列号 16 | 17 | sfMu.Lock() 18 | defer sfMu.Unlock() 19 | 20 | if sf == nil { 21 | sf = sonyflake.NewSonyflake(sonyflake.Settings{}) 22 | } 23 | 24 | return sf.NextID() 25 | } 26 | 27 | func GenerateSonyflakeID() uint64 { 28 | id, _ := NewSonyflakeID() 29 | return id 30 | } 31 | -------------------------------------------------------------------------------- /id/uuid.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/google/uuid" 7 | "github.com/lithammer/shortuuid/v4" 8 | "github.com/rs/xid" 9 | "github.com/segmentio/ksuid" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | ) 12 | 13 | func NewGUIDv4(withHyphen bool) string { 14 | id := uuid.NewString() 15 | if !withHyphen { 16 | id = strings.ReplaceAll(id, "-", "") 17 | } 18 | return id 19 | } 20 | 21 | func NewShortUUID() string { 22 | return shortuuid.New() 23 | } 24 | 25 | func NewKSUID() string { 26 | return ksuid.New().String() 27 | } 28 | 29 | func NewXID() string { 30 | return xid.New().String() 31 | } 32 | 33 | func NewMongoObjectID() string { 34 | objID := primitive.NewObjectID() 35 | return objID.String() 36 | } 37 | -------------------------------------------------------------------------------- /ioutil/file.go: -------------------------------------------------------------------------------- 1 | package ioutil 2 | 3 | import "os" 4 | 5 | // ReadFile 读取文件 6 | func ReadFile(path string) []byte { 7 | content, err := os.ReadFile(path) 8 | if err != nil { 9 | return nil 10 | } 11 | return content 12 | } 13 | -------------------------------------------------------------------------------- /ioutil/path_test.go: -------------------------------------------------------------------------------- 1 | package ioutil 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestExpandUser(t *testing.T) { 12 | assert := require.New(t) 13 | var v string 14 | var err error 15 | 16 | u, _ := user.Current() 17 | 18 | v, err = ExpandUser(`/dev/null`) 19 | assert.Equal(v, `/dev/null`) 20 | assert.Nil(err) 21 | 22 | v, err = ExpandUser(`~`) 23 | assert.Equal(v, u.HomeDir) 24 | assert.Nil(err) 25 | 26 | v, err = ExpandUser("~" + u.Name) 27 | assert.Equal(v, u.HomeDir) 28 | assert.Nil(err) 29 | 30 | v, err = ExpandUser("~/test-123") 31 | assert.Equal(v, u.HomeDir+"/test-123") 32 | assert.Nil(err) 33 | 34 | v, err = ExpandUser("~" + u.Name + "/test-123") 35 | assert.Equal(v, u.HomeDir+"/test-123") 36 | assert.Nil(err) 37 | 38 | v, err = ExpandUser("~/test-123/~/123") 39 | assert.Equal(v, u.HomeDir+"/test-123/~/123") 40 | assert.Nil(err) 41 | 42 | v, err = ExpandUser("~" + u.Name + "/test-123/~" + u.Name + "/123") 43 | assert.Equal(v, u.HomeDir+"/test-123/~"+u.Name+"/123") 44 | assert.Nil(err) 45 | 46 | assert.False(IsNonemptyFile(`/nonexistent.txt`)) 47 | assert.False(IsNonemptyDir(`/nonexistent/dir`)) 48 | 49 | assert.True(IsNonemptyFile(`/etc/hosts`)) 50 | assert.True(IsNonemptyDir(`/etc`)) 51 | 52 | x, err := os.Executable() 53 | assert.NoError(err) 54 | assert.True(IsNonemptyExecutableFile(x)) 55 | } 56 | 57 | func TestMatchPath(t *testing.T) { 58 | require.True(t, MatchPath(`**`, `/hello/there.txt`)) 59 | require.True(t, MatchPath(`*.txt`, `/hello/there.txt`)) 60 | require.True(t, MatchPath(`**/*.txt`, `/hello/there.txt`)) 61 | require.False(t, MatchPath(`**/*.txt`, `/hello/there.jpg`)) 62 | require.True(t, MatchPath("* ?at * eyes", "my cat has very bright eyes")) 63 | require.True(t, MatchPath("", "")) 64 | require.False(t, MatchPath("", "b")) 65 | require.True(t, MatchPath("*ä", "åä")) 66 | require.True(t, MatchPath("abc", "abc")) 67 | require.True(t, MatchPath("a*c", "abc")) 68 | require.True(t, MatchPath("a*c", "a12345c")) 69 | require.True(t, MatchPath("a?c", "a1c")) 70 | require.True(t, MatchPath("?at", "cat")) 71 | require.True(t, MatchPath("?at", "fat")) 72 | require.True(t, MatchPath("*", "abc")) 73 | require.True(t, MatchPath(`\*`, "*")) 74 | require.False(t, MatchPath("?at", "at")) 75 | require.True(t, MatchPath("*test", "this is a test")) 76 | require.True(t, MatchPath("this*", "this is a test")) 77 | require.True(t, MatchPath("*is *", "this is a test")) 78 | require.True(t, MatchPath("*is*a*", "this is a test")) 79 | require.True(t, MatchPath("**test**", "this is a test")) 80 | require.True(t, MatchPath("**is**a***test*", "this is a test")) 81 | require.False(t, MatchPath("*is", "this is a test")) 82 | require.False(t, MatchPath("*no*", "this is a test")) 83 | require.True(t, MatchPath("[!a]*", "this is a test3")) 84 | require.True(t, MatchPath("*abc", "abcabc")) 85 | require.True(t, MatchPath("**abc", "abcabc")) 86 | require.True(t, MatchPath("???", "abc")) 87 | require.True(t, MatchPath("?*?", "abc")) 88 | require.True(t, MatchPath("?*?", "ac")) 89 | require.False(t, MatchPath("sta", "stagnation")) 90 | require.True(t, MatchPath("sta*", "stagnation")) 91 | require.False(t, MatchPath("sta?", "stagnation")) 92 | require.False(t, MatchPath("sta?n", "stagnation")) 93 | require.True(t, MatchPath("{abc,def}ghi", "defghi")) 94 | require.True(t, MatchPath("{abc,abcd}a", "abcda")) 95 | require.True(t, MatchPath("{a,ab}{bc,f}", "abc")) 96 | require.True(t, MatchPath("{*,**}{a,b}", "ab")) 97 | require.False(t, MatchPath("{*,**}{a,b}", "ac")) 98 | require.True(t, MatchPath("/{rate,[a-z][a-z][a-z]}*", "/rate")) 99 | require.True(t, MatchPath("/{rate,[0-9][0-9][0-9]}*", "/rate")) 100 | require.True(t, MatchPath("/{rate,[a-z][a-z][a-z]}*", "/usd")) 101 | } 102 | -------------------------------------------------------------------------------- /jwtutil/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tx7do/go-utils/jwtutil 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/go-kratos/kratos/v2 v2.8.4 9 | github.com/golang-jwt/jwt/v5 v5.1.0 10 | github.com/stretchr/testify v1.10.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/kr/text v0.2.0 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | google.golang.org/protobuf v1.33.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | 21 | replace github.com/tx7do/go-utils => ../ 22 | -------------------------------------------------------------------------------- /jwtutil/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-kratos/kratos/v2 v2.8.4 h1:eIJLE9Qq9WSoKx+Buy2uPyrahtF/lPh+Xf4MTpxhmjs= 5 | github.com/go-kratos/kratos/v2 v2.8.4/go.mod h1:mq62W2101a5uYyRxe+7IdWubu7gZCGYqSNKwGFiiRcw= 6 | github.com/golang-jwt/jwt/v5 v5.1.0 h1:UGKbA/IPjtS6zLcdB7i5TyACMgSbOTiR8qzXgw8HWQU= 7 | github.com/golang-jwt/jwt/v5 v5.1.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 8 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 9 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 10 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 11 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 12 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 13 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 17 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 18 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 19 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 20 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 21 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 22 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 23 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 26 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /maputils/maputils.go: -------------------------------------------------------------------------------- 1 | // This package includes utility functions for handling and manipulating maputils. 2 | // It draws inspiration from JavaScript and Python and uses Go generics as a basis. 3 | 4 | package maputils 5 | 6 | // Keys - takes a map with keys K and values V, returns a slice of type K of the map's keys. 7 | // Note: Go maps do not preserve insertion order. 8 | func Keys[K comparable, V any](mapInstance map[K]V) []K { 9 | keys := make([]K, len(mapInstance)) 10 | 11 | i := 0 12 | for k := range mapInstance { 13 | keys[i] = k 14 | i++ 15 | } 16 | 17 | return keys 18 | } 19 | 20 | // Values - takes a map with keys K and values V, returns a slice of type V of the map's values. 21 | // Note: Go maps do not preserve insertion order. 22 | func Values[K comparable, V any](mapInstance map[K]V) []V { 23 | values := make([]V, len(mapInstance)) 24 | 25 | i := 0 26 | for _, v := range mapInstance { 27 | values[i] = v 28 | i++ 29 | } 30 | 31 | return values 32 | } 33 | 34 | // Merge - takes an arbitrary number of map instances with keys K and values V and merges them into a single map. 35 | // Note: merge works from left to right. If a key already exists in a previous map, its value is over-written. 36 | func Merge[K comparable, V any](mapInstances ...map[K]V) map[K]V { 37 | var mergedMapSize int 38 | 39 | for _, mapInstance := range mapInstances { 40 | mergedMapSize += len(mapInstance) 41 | } 42 | 43 | mergedMap := make(map[K]V, mergedMapSize) 44 | 45 | for _, mapInstance := range mapInstances { 46 | for k, v := range mapInstance { 47 | mergedMap[k] = v 48 | } 49 | } 50 | 51 | return mergedMap 52 | } 53 | 54 | // ForEach - given a map with keys K and values V, executes the passed in function for each key-value pair. 55 | func ForEach[K comparable, V any](mapInstance map[K]V, function func(key K, value V)) { 56 | for key, value := range mapInstance { 57 | function(key, value) 58 | } 59 | } 60 | 61 | // Drop - takes a map with keys K and values V, and a slice of keys K, dropping all the key-value pairs that match the keys in the slice. 62 | // Note: this function will modify the passed in map. To get a different object, use the Copy function to pass a copy to this function. 63 | func Drop[K comparable, V any](mapInstance map[K]V, keys []K) map[K]V { 64 | for _, key := range keys { 65 | delete(mapInstance, key) 66 | } 67 | 68 | return mapInstance 69 | } 70 | 71 | // Copy - takes a map with keys K and values V and returns a copy of the map. 72 | func Copy[K comparable, V any](mapInstance map[K]V) map[K]V { 73 | mapCopy := make(map[K]V, len(mapInstance)) 74 | 75 | for key, value := range mapInstance { 76 | mapCopy[key] = value 77 | } 78 | 79 | return mapCopy 80 | } 81 | 82 | // Filter - takes a map with keys K and values V, and executes the passed in function for each key-value pair. 83 | // If the filter function returns true, the key-value pair will be included in the output, otherwise it is filtered out. 84 | func Filter[K comparable, V any](mapInstance map[K]V, function func(key K, value V) bool) map[K]V { 85 | mapCopy := make(map[K]V, len(mapInstance)) 86 | 87 | for key, value := range mapInstance { 88 | if function(key, value) { 89 | mapCopy[key] = value 90 | } 91 | } 92 | 93 | return mapCopy 94 | } 95 | -------------------------------------------------------------------------------- /maputils/maputils_test.go: -------------------------------------------------------------------------------- 1 | package maputils_test 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/tx7do/go-utils/maputils" 9 | ) 10 | 11 | func TestKeys(t *testing.T) { 12 | var daysMap = map[string]int{ 13 | "Sunday": 1, 14 | "Monday": 2, 15 | "Tuesday": 3, 16 | "Wednesday": 4, 17 | "Thursday": 5, 18 | "Friday": 6, 19 | "Saturday": 7, 20 | } 21 | keys := maputils.Keys(daysMap) 22 | days := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} 23 | sort.Strings(days) 24 | sort.Strings(keys) 25 | assert.Equal(t, days, keys) 26 | } 27 | 28 | func TestValues(t *testing.T) { 29 | var daysMap = map[string]int{ 30 | "Sunday": 1, 31 | "Monday": 2, 32 | "Tuesday": 3, 33 | "Wednesday": 4, 34 | "Thursday": 5, 35 | "Friday": 6, 36 | "Saturday": 7, 37 | } 38 | values := maputils.Values(daysMap) 39 | sort.Ints(values) 40 | assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7}, values) 41 | } 42 | 43 | func TestMerge(t *testing.T) { 44 | first := map[string]string{ 45 | "George": "Harrison", 46 | "Betty": "Davis", 47 | } 48 | second := map[string]string{ 49 | "Ronald": "Reagen", 50 | "Betty": "Stewart", 51 | } 52 | 53 | assert.Equal(t, 54 | map[string]string{ 55 | "George": "Harrison", 56 | "Betty": "Stewart", 57 | "Ronald": "Reagen", 58 | }, maputils.Merge(first, second)) 59 | } 60 | 61 | func TestForEach(t *testing.T) { 62 | var daysMap = map[string]int{ 63 | "Sunday": 1, 64 | "Monday": 2, 65 | "Tuesday": 3, 66 | "Wednesday": 4, 67 | "Thursday": 5, 68 | "Friday": 6, 69 | "Saturday": 7, 70 | } 71 | 72 | sum := 0 73 | 74 | maputils.ForEach(daysMap, func(_ string, day int) { 75 | sum += day 76 | }) 77 | 78 | assert.Equal(t, 28, sum) 79 | } 80 | 81 | func TestDrop(t *testing.T) { 82 | var daysMap = map[string]int{ 83 | "Sunday": 1, 84 | "Monday": 2, 85 | "Tuesday": 3, 86 | "Wednesday": 4, 87 | "Thursday": 5, 88 | "Friday": 6, 89 | "Saturday": 7, 90 | } 91 | 92 | expectedResult := map[string]int{ 93 | "Sunday": 1, 94 | "Friday": 6, 95 | } 96 | assert.Equal(t, expectedResult, maputils.Drop(daysMap, []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Saturday"})) 97 | // ensure we do not modify the original value 98 | assert.Equal(t, expectedResult, daysMap) 99 | } 100 | 101 | func TestCopy(t *testing.T) { 102 | var daysMap = map[string]int{ 103 | "Sunday": 1, 104 | "Monday": 2, 105 | "Tuesday": 3, 106 | "Wednesday": 4, 107 | "Thursday": 5, 108 | "Friday": 6, 109 | "Saturday": 7, 110 | } 111 | daysCopy := maputils.Copy(daysMap) 112 | assert.Equal(t, daysMap, daysCopy) 113 | delete(daysCopy, "Sunday") 114 | assert.NotEqual(t, daysMap, daysCopy) 115 | } 116 | 117 | func TestFilter(t *testing.T) { 118 | var daysMap = map[string]int{ 119 | "Sunday": 1, 120 | "Monday": 2, 121 | "Tuesday": 3, 122 | "Wednesday": 4, 123 | "Thursday": 5, 124 | "Friday": 6, 125 | "Saturday": 7, 126 | } 127 | 128 | var expectedResult = map[string]int{ 129 | "Monday": 2, 130 | "Wednesday": 4, 131 | "Friday": 6, 132 | } 133 | 134 | filteredDays := maputils.Filter(daysMap, func(_ string, value int) bool { 135 | return value%2 == 0 136 | }) 137 | 138 | assert.Equal(t, expectedResult, filteredDays) 139 | } 140 | -------------------------------------------------------------------------------- /math/gaussian.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // prop 8 | //mean: the mean (μ) of the distribution 9 | //variance: the variance (σ^2) of the distribution 10 | //standardDeviation: the standard deviation (σ) of the distribution 11 | 12 | // combination 13 | 14 | type Gaussian struct { 15 | mean float64 16 | variance float64 17 | standardDeviation float64 18 | } 19 | 20 | func NewGaussian(mean, variance float64) *Gaussian { 21 | if variance <= 0.0 { 22 | panic("error") 23 | } 24 | 25 | return &Gaussian{ 26 | mean: mean, 27 | variance: variance, 28 | standardDeviation: math.Sqrt(float64(variance)), 29 | } 30 | } 31 | 32 | // Erfc Complementary error function 33 | // From Numerical Recipes in C 2e p221 34 | func Erfc(x float64) float64 { 35 | z := math.Abs(x) 36 | t := 1 / (1 + z/2) 37 | r := t * math.Exp(-z*z-1.26551223+t*(1.00002368+ 38 | t*(0.37409196+t*(0.09678418+t*(-0.18628806+ 39 | t*(0.27886807+t*(-1.13520398+t*(1.48851587+ 40 | t*(-0.82215223+t*0.17087277))))))))) 41 | if x >= 0 { 42 | return r 43 | } else { 44 | return 2 - r 45 | } 46 | } 47 | 48 | // Ierfc Inverse complementary error function 49 | // From Numerical Recipes 3e p265 50 | func Ierfc(x float64) float64 { 51 | if x >= 2 { 52 | return -100 53 | } 54 | if x <= 0 { 55 | return 100 56 | } 57 | var xx float64 58 | if x < 1 { 59 | xx = x 60 | } else { 61 | xx = 2 - x 62 | } 63 | t := math.Sqrt(-2 * math.Log(xx/2)) 64 | r := -0.70711 * ((2.30753+t*0.27061)/ 65 | (1+t*(0.99229+t*0.04481)) - t) 66 | 67 | for j := 0; j < 2; j++ { 68 | e := Erfc(r) - xx 69 | r += e / (1.12837916709551257*math.Exp(-(r*r)) - r*e) 70 | } 71 | 72 | if x < 1 { 73 | return r 74 | } else { 75 | return -r 76 | } 77 | 78 | } 79 | 80 | // fromPrecisionMean Construct a new distribution from the precision and precisionmean 81 | func fromPrecisionMean(precision, precisionmean float64) *Gaussian { 82 | return NewGaussian(precisionmean/precision, 1/precision) 83 | } 84 | 85 | /// PROB 86 | 87 | // Pdf pdf(x): the probability density function, which describes the probability 88 | // of a random variable taking on the value x 89 | func (g *Gaussian) Pdf(x float64) float64 { 90 | m := g.standardDeviation * math.Sqrt(2*math.Pi) 91 | e := math.Exp(-math.Pow(x-g.mean, 2) / (2 * g.variance)) 92 | return e / m 93 | } 94 | 95 | // Cdf cdf(x): the cumulative distribution function, 96 | // which describes the probability of a random 97 | // variable falling in the interval (−∞, x] 98 | func (g *Gaussian) Cdf(x float64) float64 { 99 | return 0.5 * Erfc(-(x-g.mean)/(g.standardDeviation*math.Sqrt(2))) 100 | } 101 | 102 | // Ppf ppf(x): the percent point function, the inverse of cdf 103 | func (g *Gaussian) Ppf(x float64) float64 { 104 | return g.mean - g.standardDeviation*math.Sqrt(2)*Ierfc(2*x) 105 | } 106 | 107 | // Add add(d): returns the result of adding this and the given distribution 108 | func (g *Gaussian) Add(d *Gaussian) *Gaussian { 109 | return NewGaussian(g.mean+d.mean, g.variance+d.variance) 110 | } 111 | 112 | // Sub sub(d): returns the result of subtracting this and the given distribution 113 | func (g *Gaussian) Sub(d *Gaussian) *Gaussian { 114 | return NewGaussian(g.mean-d.mean, g.variance+d.variance) 115 | } 116 | 117 | // Scale scale(c): returns the result of scaling this distribution by the given constant 118 | func (g *Gaussian) Scale(c float64) *Gaussian { 119 | return NewGaussian(g.mean*c, g.variance*c*c) 120 | } 121 | 122 | // Mul mul(d): returns the product distribution of this and the given distribution. If a constant is passed in the distribution is scaled. 123 | func (g *Gaussian) Mul(d *Gaussian) *Gaussian { 124 | precision := 1 / g.variance 125 | dprecision := 1 / d.variance 126 | return fromPrecisionMean(precision+dprecision, precision*g.mean+dprecision*d.mean) 127 | } 128 | 129 | // Div div(d): returns the quotient distribution of this and the given distribution. If a constant is passed in the distribution is scaled by 1/d. 130 | func (g *Gaussian) Div(d *Gaussian) *Gaussian { 131 | precision := 1 / g.variance 132 | dprecision := 1 / d.variance 133 | return fromPrecisionMean(precision-dprecision, precision*g.mean-dprecision*d.mean) 134 | } 135 | -------------------------------------------------------------------------------- /math/gaussian_test.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestGaussian(t *testing.T) { 9 | g := NewGaussian(3.0, 1) 10 | 11 | fmt.Printf("g: %#v\n", g) 12 | fmt.Printf("pdf: %f\n", g.Pdf(5)) 13 | fmt.Printf("cdf: %f\n", g.Cdf(2)) 14 | fmt.Printf("ppf: %f\n", g.Ppf(5)) 15 | 16 | d := NewGaussian(0, 1) 17 | fmt.Printf("ppf: %f, %f\n", d.Pdf(-2), 0.053991) 18 | fmt.Printf("ppf: %f, %f\n", d.Pdf(-1), 0.241971) 19 | fmt.Printf("ppf: %f, %f\n", d.Pdf(0), 0.398942) 20 | fmt.Printf("ppf: %f, %f\n", d.Pdf(1), 0.241971) 21 | fmt.Printf("ppf: %f, %f\n", d.Pdf(2), 0.053991) 22 | 23 | fmt.Printf("cdf: %f, %f\n", d.Cdf(-1.28155), 0.1) 24 | fmt.Printf("cdf: %f, %f\n", d.Cdf(-0.67499), 0.25) 25 | fmt.Printf("cdf: %f, %f\n", d.Cdf(0), 0.5) 26 | fmt.Printf("cdf: %f, %f\n", d.Cdf(0.67499), 0.75) 27 | fmt.Printf("cdf: %f, %f\n", d.Cdf(1.28155), 0.9) 28 | 29 | fmt.Printf("ppf: %f, %f\n", d.Ppf(0.1), -1.28155) 30 | fmt.Printf("ppf: %f, %f\n", d.Ppf(0.25), -0.67499) 31 | fmt.Printf("ppf: %f, %f\n", d.Ppf(0.5), 0.0) 32 | fmt.Printf("ppf: %f, %f\n", d.Ppf(0.75), 0.67449) 33 | fmt.Printf("ppf: %f, %f\n", d.Ppf(0.9), 1.28155) 34 | 35 | d = d.Mul(NewGaussian(0, 1)) 36 | fmt.Printf("Mul: %#v\n", d) 37 | fmt.Printf("%#v\n%#v", NewGaussian(1, 1).Scale(2), NewGaussian(2, 4)) 38 | 39 | d = NewGaussian(1, 1).Div(NewGaussian(1, 2)) 40 | fmt.Printf("div\n") 41 | fmt.Printf("%#v\n%#v\n", d, NewGaussian(1, 2)) 42 | fmt.Printf("%#v\n%#v\n", NewGaussian(1, 1).Scale(1/(1.0/2.0)), NewGaussian(2, 4)) 43 | 44 | fmt.Printf("ADD:\n%#v\n%#v\n", NewGaussian(1, 1).Add(NewGaussian(1, 2)), NewGaussian(2, 3)) 45 | fmt.Printf("SUB:\n%#v\n%#v\n", NewGaussian(1, 1).Sub(NewGaussian(1, 2)), NewGaussian(0, 3)) 46 | fmt.Printf("SCALE:\n%#v\n%#v\n", NewGaussian(1, 1).Scale(2), NewGaussian(2, 4)) 47 | } 48 | -------------------------------------------------------------------------------- /math/math.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // Sign 符号函数(Sign function,简称sgn)是一个逻辑函数,用以判断实数的正负号。为避免和英文读音相似的正弦函数(sine)混淆,它亦称为Signum function。 8 | func Sign[T int | int8 | int16 | int32 | int64 | float32 | float64](x T) T { 9 | switch { 10 | case x < 0: // x < 0 : -1 11 | return -1 12 | case x > 0: // x > 0 : +1 13 | return +1 14 | default: // x == 0 : 0 15 | return 0 16 | } 17 | } 18 | 19 | // Mean 计算给定数据的平均值 20 | func Mean(num []float64) float64 { 21 | var count = len(num) 22 | var sum float64 = 0 23 | for i := 0; i < count; i++ { 24 | sum += num[i] 25 | } 26 | return sum / float64(count) 27 | } 28 | 29 | // Variance 使用平均值计算给定数据的方差 30 | func Variance(mean float64, num []float64) float64 { 31 | var count = len(num) 32 | var variance float64 = 0 33 | for i := 0; i < count; i++ { 34 | variance += math.Pow(num[i]-mean, 2) 35 | } 36 | return variance / float64(count) 37 | } 38 | 39 | // StandardDeviation 使用方差计算给定数据的标准偏差 40 | func StandardDeviation(num []float64) float64 { 41 | var mean = Mean(num) 42 | var variance = Variance(mean, num) 43 | return math.Sqrt(variance) 44 | } 45 | 46 | // SumInt 计算整数数组的和 47 | func SumInt[T int | int32 | int64](array []T) int64 { 48 | var sum int64 49 | for _, v := range array { 50 | sum = sum + int64(v) 51 | } 52 | return sum 53 | } 54 | 55 | // SumUint 计算整数数组的和 56 | func SumUint[T uint | uint32 | uint64](array []T) uint64 { 57 | var sum uint64 58 | for _, v := range array { 59 | sum = sum + uint64(v) 60 | } 61 | return sum 62 | } 63 | 64 | // SumFloat 计算浮点数数组的和 65 | func SumFloat[T float32 | float64](array []T) float64 { 66 | var sum float64 67 | for _, v := range array { 68 | sum = sum + float64(v) 69 | } 70 | return sum 71 | } 72 | -------------------------------------------------------------------------------- /math/math_test.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSign(t *testing.T) { 10 | assert.True(t, Sign(2) == 1) 11 | assert.True(t, Sign(-2) == -1) 12 | assert.True(t, Sign(0) == 0) 13 | 14 | assert.True(t, Sign(int64(2)) == 1) 15 | assert.True(t, Sign(int64(-2)) == -1) 16 | assert.True(t, Sign(int64(0)) == 0) 17 | 18 | assert.True(t, Sign(float32(2)) == 1) 19 | assert.True(t, Sign(float32(-2)) == -1) 20 | assert.True(t, Sign(float32(0)) == 0) 21 | 22 | assert.True(t, Sign(float64(2)) == 1) 23 | assert.True(t, Sign(float64(-2)) == -1) 24 | assert.True(t, Sign(float64(0)) == 0) 25 | } 26 | 27 | func TestStandardDeviation(t *testing.T) { 28 | assert.Equal(t, StandardDeviation([]float64{3, 5, 9, 1, 8, 6, 58, 9, 4, 10}), 15.8117045254457) 29 | assert.Equal(t, StandardDeviation([]float64{1, 3, 5, 7, 9, 11, 2, 4, 6, 8}), 3.0397368307141326) 30 | } 31 | -------------------------------------------------------------------------------- /pagination/pagination.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | const ( 4 | DefaultPage = 1 // 默认页数 5 | DefaultPageSize = 10 // 默认每页行数 6 | ) 7 | 8 | // GetPageOffset 计算偏移量 9 | func GetPageOffset(pageNum, pageSize int32) int { 10 | return int((pageNum - 1) * pageSize) 11 | } 12 | -------------------------------------------------------------------------------- /rand/rand.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "github.com/tx7do/go-utils/math" 5 | "math/rand" 6 | ) 7 | 8 | var rnd = rand.New(rand.NewSource(Seed(UnixNanoSeed))) 9 | 10 | func init() { 11 | if rnd != nil { 12 | rnd = rand.New(rand.NewSource(Seed(UnixNanoSeed))) 13 | } 14 | } 15 | 16 | func Float32() float32 { 17 | return rnd.Float32() 18 | } 19 | 20 | func Float64() float64 { 21 | return rnd.Float64() 22 | } 23 | 24 | func Intn(n int) int { 25 | return rnd.Intn(n) 26 | } 27 | 28 | func Int31n(n int32) int32 { 29 | return rnd.Int31n(n) 30 | } 31 | 32 | func Int63n(n int64) int64 { 33 | return rnd.Int63n(n) 34 | } 35 | 36 | // RandomInt 根据区间产生随机数 37 | func RandomInt(min, max int) int { 38 | if min >= max { 39 | return max 40 | } 41 | return min + Intn(max-min+1) 42 | } 43 | 44 | // RandomInt32 根据区间产生随机数 45 | func RandomInt32(min, max int32) int32 { 46 | if min >= max { 47 | return max 48 | } 49 | return min + Int31n(max-min+1) 50 | } 51 | 52 | // RandomInt64 根据区间产生随机数 53 | func RandomInt64(min, max int64) int64 { 54 | if min >= max { 55 | return max 56 | } 57 | return min + Int63n(max-min+1) 58 | } 59 | 60 | // RandomString 随机字符串,包含大小写字母和数字 61 | func RandomString(l int) string { 62 | if l <= 0 { 63 | return "" 64 | } 65 | 66 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 67 | 68 | bytes := make([]byte, l) 69 | for i := 0; i < l; i++ { 70 | bytes[i] = charset[Intn(len(charset))] 71 | } 72 | 73 | return string(bytes) 74 | } 75 | 76 | // RandomChoice 随机选择数组中的元素 77 | func RandomChoice[T any](array []T, n int) []T { 78 | if n <= 0 { 79 | return nil 80 | } 81 | if n == 1 { 82 | return []T{array[Intn(len(array))]} 83 | } 84 | 85 | tmp := make([]T, len(array)) 86 | copy(tmp, array) 87 | if len(tmp) <= n { 88 | return tmp 89 | } 90 | 91 | Shuffle(tmp) 92 | 93 | return tmp[:n] 94 | } 95 | 96 | // Shuffle 随机打乱数组 97 | func Shuffle[T any](array []T) { 98 | if array == nil { 99 | return 100 | } 101 | 102 | for i := range array { 103 | j := Intn(i + 1) 104 | array[i], array[j] = array[j], array[i] 105 | } 106 | } 107 | 108 | // WeightedChoice 根据权重随机,返回对应选项的索引,O(n) 109 | func WeightedChoice(weightArray []int) int { 110 | if weightArray == nil { 111 | return -1 112 | } 113 | 114 | total := math.SumInt(weightArray) 115 | rv := Int63n(total) 116 | for i, v := range weightArray { 117 | if rv < int64(v) { 118 | return i 119 | } 120 | rv -= int64(v) 121 | } 122 | 123 | return len(weightArray) - 1 124 | } 125 | 126 | // NonWeightedChoice 根据权重随机,返回对应选项的索引,O(n). 权重大于等于0 127 | func NonWeightedChoice(weightArray []int) int { 128 | if weightArray == nil { 129 | return -1 130 | } 131 | 132 | for i, weight := range weightArray { 133 | if weight < 0 { 134 | weightArray[i] = 0 135 | } 136 | } 137 | 138 | total := math.SumInt(weightArray) 139 | rv := Int63n(total) 140 | for i, v := range weightArray { 141 | if rv < int64(v) { 142 | return i 143 | } 144 | rv -= int64(v) 145 | } 146 | 147 | return len(weightArray) - 1 148 | } 149 | -------------------------------------------------------------------------------- /rand/randomizer.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "github.com/tx7do/go-utils/math" 5 | "math/rand" 6 | ) 7 | 8 | type Randomizer struct { 9 | rnd *rand.Rand 10 | } 11 | 12 | func NewRandomizer(seedType SeedType) *Randomizer { 13 | return &Randomizer{ 14 | rnd: rand.New(rand.NewSource(Seed(seedType))), 15 | } 16 | } 17 | 18 | func (r *Randomizer) Float32() float32 { 19 | return r.rnd.Float32() 20 | } 21 | 22 | func (r *Randomizer) Float64() float64 { 23 | return r.rnd.Float64() 24 | } 25 | 26 | func (r *Randomizer) Int() int { 27 | return r.rnd.Int() 28 | } 29 | 30 | func (r *Randomizer) Int31() int32 { 31 | return r.rnd.Int31() 32 | } 33 | 34 | func (r *Randomizer) Int63() int64 { 35 | return r.rnd.Int63() 36 | } 37 | 38 | func (r *Randomizer) Uint32() uint32 { 39 | return r.rnd.Uint32() 40 | } 41 | 42 | func (r *Randomizer) Uint64() uint64 { 43 | return r.rnd.Uint64() 44 | } 45 | 46 | func (r *Randomizer) Intn(n int) int { 47 | return r.rnd.Intn(n) 48 | } 49 | 50 | func (r *Randomizer) Int31n(n int32) int32 { 51 | return r.rnd.Int31n(n) 52 | } 53 | 54 | func (r *Randomizer) Int63n(n int64) int64 { 55 | return r.rnd.Int63n(n) 56 | } 57 | 58 | // RangeInt 根据区间产生随机数 59 | func (r *Randomizer) RangeInt(min, max int) int { 60 | if min >= max { 61 | return max 62 | } 63 | return min + r.Intn(max-min+1) 64 | } 65 | 66 | // RangeInt32 根据区间产生随机数 67 | func (r *Randomizer) RangeInt32(min, max int32) int32 { 68 | if min >= max { 69 | return max 70 | } 71 | return min + r.Int31n(max-min+1) 72 | } 73 | 74 | // RangeInt64 根据区间产生随机数 75 | func (r *Randomizer) RangeInt64(min, max int64) int64 { 76 | if min >= max { 77 | return max 78 | } 79 | return min + r.Int63n(max-min+1) 80 | } 81 | 82 | // RangeUint 根据区间产生随机数 83 | func (r *Randomizer) RangeUint(min, max uint) uint { 84 | if min >= max { 85 | return max 86 | } 87 | return min + uint(r.Intn(int(max-min+1))) 88 | } 89 | 90 | // RangeUint32 根据区间产生随机数 91 | func (r *Randomizer) RangeUint32(min, max uint32) uint32 { 92 | if min >= max { 93 | return max 94 | } 95 | return min + uint32(r.Int31n(int32(max-min+1))) 96 | } 97 | 98 | // RangeUint64 根据区间产生随机数 99 | func (r *Randomizer) RangeUint64(min, max uint64) uint64 { 100 | if min >= max { 101 | return max 102 | } 103 | return min + uint64(r.Int63n(int64(max-min+1))) 104 | } 105 | 106 | // RangeFloat32 根据区间产生随机数 107 | func (r *Randomizer) RangeFloat32(min, max float32) float32 { 108 | if min >= max { 109 | return max 110 | } 111 | return min + r.Float32()*(max-min) 112 | } 113 | 114 | // RangeFloat64 根据区间产生随机数 115 | func (r *Randomizer) RangeFloat64(min, max float64) float64 { 116 | if min >= max { 117 | return max 118 | } 119 | return min + r.Float64()*(max-min) 120 | } 121 | 122 | // RandomString 随机字符串,包含大小写字母和数字 123 | func (r *Randomizer) RandomString(l int) string { 124 | bytes := make([]byte, l) 125 | for i := 0; i < l; i++ { 126 | x := r.Intn(3) 127 | switch x { 128 | case 0: 129 | bytes[i] = byte(r.RangeInt(65, 90)) //大写字母 130 | case 1: 131 | bytes[i] = byte(r.RangeInt(97, 122)) 132 | case 2: 133 | bytes[i] = byte(r.Intn(10)) 134 | } 135 | } 136 | return string(bytes) 137 | } 138 | 139 | // WeightedChoice 根据权重随机,返回对应选项的索引,O(n) 140 | func (r *Randomizer) WeightedChoice(weightArray []int) int { 141 | if weightArray == nil { 142 | return -1 143 | } 144 | 145 | total := math.SumInt(weightArray) 146 | rv := r.Int63n(total) 147 | for i, v := range weightArray { 148 | if rv < int64(v) { 149 | return i 150 | } 151 | rv -= int64(v) 152 | } 153 | 154 | return len(weightArray) - 1 155 | } 156 | 157 | // NonWeightedChoice 根据权重随机,返回对应选项的索引,O(n). 权重大于等于0 158 | func (r *Randomizer) NonWeightedChoice(weightArray []int) int { 159 | if weightArray == nil { 160 | return -1 161 | } 162 | 163 | for i, weight := range weightArray { 164 | if weight < 0 { 165 | weightArray[i] = 0 166 | } 167 | } 168 | 169 | total := math.SumInt(weightArray) 170 | rv := r.Int63n(total) 171 | for i, v := range weightArray { 172 | if rv < int64(v) { 173 | return i 174 | } 175 | rv -= int64(v) 176 | } 177 | 178 | return len(weightArray) - 1 179 | } 180 | -------------------------------------------------------------------------------- /rand/randomizer_test.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestRandomizer_RangeUint32_MinEqualsMax(t *testing.T) { 9 | r := NewRandomizer(UnixNanoSeed) 10 | result := r.RangeUint32(5, 5) 11 | assert.Equal(t, uint32(5), result) 12 | } 13 | 14 | func TestRandomizer_RangeUint32_MinGreaterThanMax(t *testing.T) { 15 | r := NewRandomizer(UnixNanoSeed) 16 | result := r.RangeUint32(10, 5) 17 | assert.Equal(t, uint32(5), result) 18 | } 19 | 20 | func TestRandomizer_RangeUint32_PositiveRange(t *testing.T) { 21 | r := NewRandomizer(UnixNanoSeed) 22 | for i := 0; i < 1000; i++ { 23 | result := r.RangeUint32(1, 10) 24 | assert.True(t, result >= 1) 25 | assert.True(t, result <= 10) 26 | } 27 | } 28 | 29 | func TestRandomizer_RangeUint64_MinEqualsMax(t *testing.T) { 30 | r := NewRandomizer(UnixNanoSeed) 31 | result := r.RangeUint64(5, 5) 32 | assert.Equal(t, uint64(5), result) 33 | } 34 | 35 | func TestRandomizer_RangeUint64_MinGreaterThanMax(t *testing.T) { 36 | r := NewRandomizer(UnixNanoSeed) 37 | result := r.RangeUint64(10, 5) 38 | assert.Equal(t, uint64(5), result) 39 | } 40 | 41 | func TestRandomizer_RangeUint64_PositiveRange(t *testing.T) { 42 | r := NewRandomizer(UnixNanoSeed) 43 | for i := 0; i < 1000; i++ { 44 | result := r.RangeUint64(1, 10) 45 | assert.True(t, result >= 1) 46 | assert.True(t, result <= 10) 47 | } 48 | } 49 | 50 | func TestRandomizer_WeightedChoice_EmptyArray(t *testing.T) { 51 | r := NewRandomizer(UnixNanoSeed) 52 | result := r.WeightedChoice([]int{}) 53 | assert.Equal(t, -1, result) 54 | } 55 | 56 | func TestRandomizer_WeightedChoice_AllZeroWeights(t *testing.T) { 57 | r := NewRandomizer(UnixNanoSeed) 58 | result := r.WeightedChoice([]int{0, 0, 0}) 59 | assert.Equal(t, 2, result) 60 | } 61 | 62 | func TestRandomizer_WeightedChoice_MixedWeights(t *testing.T) { 63 | r := NewRandomizer(UnixNanoSeed) 64 | weightArray := []int{1, 0, 3, 0, 2} 65 | counts := make([]int, len(weightArray)) 66 | for i := 0; i < 1000; i++ { 67 | choice := r.WeightedChoice(weightArray) 68 | counts[choice]++ 69 | } 70 | assert.Greater(t, counts[0], 0) 71 | assert.Greater(t, counts[2], 0) 72 | assert.Greater(t, counts[4], 0) 73 | } 74 | 75 | func TestRandomizer_RandomString_LengthZero(t *testing.T) { 76 | r := NewRandomizer(UnixNanoSeed) 77 | result := r.RandomString(0) 78 | assert.Equal(t, "", result) 79 | } 80 | 81 | func TestRandomizer_RandomString_CorrectLength(t *testing.T) { 82 | r := NewRandomizer(UnixNanoSeed) 83 | length := 50 84 | result := r.RandomString(length) 85 | assert.Equal(t, length, len(result)) 86 | } 87 | -------------------------------------------------------------------------------- /rand/seeder.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | cryptoRand "crypto/rand" 5 | "encoding/binary" 6 | "golang.org/x/exp/rand" 7 | "hash/maphash" 8 | mathRand "math/rand" 9 | "runtime" 10 | "time" 11 | ) 12 | 13 | type SeedType string 14 | 15 | const ( 16 | UnixNanoSeed SeedType = "UnixNano" 17 | MapHashSeed SeedType = "MapHash" 18 | CryptoRandSeed SeedType = "CryptoRand" 19 | RandomStringSeed SeedType = "RandomString" 20 | ) 21 | 22 | type Seeder struct { 23 | seedType SeedType 24 | } 25 | 26 | func NewSeeder(seedType SeedType) *Seeder { 27 | return &Seeder{ 28 | seedType: seedType, 29 | } 30 | } 31 | 32 | func (r *Seeder) UnixNano() int64 { 33 | // 获取当前时间戳 34 | timestamp := time.Now().UnixNano() 35 | 36 | // 生成一个随机数 37 | var randomBytes [8]byte 38 | _, err := rand.Read(randomBytes[:]) 39 | if err != nil { 40 | panic("failed to generate random bytes") 41 | } 42 | randomPart := int64(binary.LittleEndian.Uint64(randomBytes[:])) 43 | 44 | // 获取 Goroutine ID(或其他唯一标识) 45 | goroutineID := int64(runtime.NumGoroutine()) 46 | 47 | // 结合时间戳、随机数和 Goroutine ID 48 | seed := timestamp ^ randomPart ^ goroutineID 49 | 50 | return seed 51 | } 52 | 53 | func (r *Seeder) MapHash() int64 { 54 | return int64(new(maphash.Hash).Sum64()) 55 | } 56 | 57 | func (r *Seeder) CryptoRand() int64 { 58 | var b [8]byte 59 | _, err := cryptoRand.Read(b[:]) 60 | if err != nil { 61 | panic("cannot seed math/rand package with cryptographically secure random number generator") 62 | } 63 | seed := int64(binary.LittleEndian.Uint64(b[:])) 64 | return seed 65 | } 66 | 67 | var Alpha = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" 68 | 69 | func (r *Seeder) RandomString() int64 { 70 | const size = 8 71 | buf := make([]byte, size) 72 | for i := 0; i < size; i++ { 73 | buf[i] = Alpha[mathRand.Intn(len(Alpha))] 74 | } 75 | seed := int64(binary.LittleEndian.Uint64(buf[:])) 76 | 77 | return seed 78 | } 79 | 80 | func (r *Seeder) Seed() int64 { 81 | switch r.seedType { 82 | default: 83 | fallthrough 84 | case UnixNanoSeed: 85 | return r.UnixNano() 86 | case MapHashSeed: 87 | return r.MapHash() 88 | case CryptoRandSeed: 89 | return r.CryptoRand() 90 | case RandomStringSeed: 91 | return r.RandomString() 92 | } 93 | } 94 | 95 | // Seed generates a seed based on the specified SeedType. 96 | func Seed(seedType SeedType) int64 { 97 | randomizer := NewSeeder(seedType) 98 | return randomizer.Seed() 99 | } 100 | -------------------------------------------------------------------------------- /rand/seeder_test.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestUnixNanoSeed(t *testing.T) { 9 | seeder := NewSeeder(UnixNanoSeed) 10 | 11 | var seeds = make(map[int64]bool) 12 | for i := 0; i < 100000; i++ { 13 | seed := seeder.Seed() 14 | seeds[seed] = true 15 | } 16 | fmt.Println("UnixNano Seed", len(seeds)) 17 | } 18 | 19 | func TestMapHashSeed(t *testing.T) { 20 | seeder := NewSeeder(MapHashSeed) 21 | 22 | var seeds = make(map[int64]bool) 23 | for i := 0; i < 100000; i++ { 24 | seed := seeder.Seed() 25 | seeds[seed] = true 26 | } 27 | fmt.Println("MapHash Seed", len(seeds)) 28 | } 29 | 30 | func TestCryptoRandSeed(t *testing.T) { 31 | seeder := NewSeeder(CryptoRandSeed) 32 | 33 | var seeds = make(map[int64]bool) 34 | for i := 0; i < 100000; i++ { 35 | seed := seeder.Seed() 36 | seeds[seed] = true 37 | } 38 | fmt.Println("CryptoRand Seed", len(seeds)) 39 | } 40 | 41 | func TestRandomStringSeed(t *testing.T) { 42 | seeder := NewSeeder(RandomStringSeed) 43 | 44 | var seeds = make(map[int64]bool) 45 | for i := 0; i < 100000; i++ { 46 | seed := seeder.Seed() 47 | seeds[seed] = true 48 | } 49 | fmt.Println("RandomString Seed", len(seeds)) 50 | } 51 | 52 | func TestSeed(t *testing.T) { 53 | var seeds = make(map[int64]bool) 54 | for i := 0; i < 100000; i++ { 55 | seed := Seed(UnixNanoSeed) 56 | seeds[seed] = true 57 | } 58 | fmt.Println("UnixNano Seed", len(seeds)) 59 | 60 | seeds = make(map[int64]bool) 61 | for i := 0; i < 100000; i++ { 62 | seed := Seed(MapHashSeed) 63 | seeds[seed] = true 64 | } 65 | fmt.Println("MapHash Seed", len(seeds)) 66 | 67 | seeds = make(map[int64]bool) 68 | for i := 0; i < 100000; i++ { 69 | seed := Seed(CryptoRandSeed) 70 | seeds[seed] = true 71 | } 72 | fmt.Println("CryptoRand Seed", len(seeds)) 73 | 74 | seeds = make(map[int64]bool) 75 | for i := 0; i < 100000; i++ { 76 | seed := Seed(RandomStringSeed) 77 | seeds[seed] = true 78 | } 79 | fmt.Println("RandomString Seed", len(seeds)) 80 | } 81 | -------------------------------------------------------------------------------- /slug/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tx7do/go-utils/slug 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/gosimple/slug v1.15.0 9 | github.com/stretchr/testify v1.10.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/gosimple/unidecode v1.0.1 // indirect 15 | github.com/kr/pretty v0.3.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/rogpeppe/go-internal v1.13.1 // indirect 18 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | 22 | replace github.com/tx7do/go-utils => ../ 23 | -------------------------------------------------------------------------------- /slug/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= 5 | github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= 6 | github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= 7 | github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= 8 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 9 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 10 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 19 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 20 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 21 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 22 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 25 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | -------------------------------------------------------------------------------- /slug/slug.go: -------------------------------------------------------------------------------- 1 | package slug 2 | 3 | import ( 4 | "github.com/gosimple/slug" 5 | ) 6 | 7 | // Generate 生成短链接 8 | func Generate(input string) string { 9 | slug.Lowercase = true 10 | return slug.MakeLang(input, "en") 11 | } 12 | 13 | // GenerateCaseSensitive 生成大小写敏感的短链接 14 | func GenerateCaseSensitive(input string) string { 15 | slug.Lowercase = false 16 | return slug.MakeLang(input, "en") 17 | } 18 | 19 | // GenerateEnglish 生成英文短链接 20 | func GenerateEnglish(input string) string { 21 | slug.Lowercase = true 22 | return slug.MakeLang(input, "en") 23 | } 24 | 25 | // GenerateGerman 生成德文短链接 26 | func GenerateGerman(input string) string { 27 | slug.Lowercase = true 28 | return slug.MakeLang(input, "de") 29 | } 30 | -------------------------------------------------------------------------------- /slug/slug_test.go: -------------------------------------------------------------------------------- 1 | package slug 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gosimple/slug" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGoSimple(t *testing.T) { 12 | // 俄文 13 | text := slug.Make("Hellö Wörld хелло ворлд") 14 | assert.Equal(t, text, "hello-world-khello-vorld") 15 | fmt.Println(text) 16 | 17 | // 繁体中文 18 | someText := slug.Make("影師") 19 | assert.Equal(t, someText, "ying-shi") 20 | fmt.Println(someText) 21 | 22 | // 简体中文 23 | cnText := slug.Make("天下太平") 24 | assert.Equal(t, cnText, "tian-xia-tai-ping") 25 | fmt.Println(cnText) 26 | 27 | // 英文 28 | enText := slug.MakeLang("This & that", "en") 29 | assert.Equal(t, enText, "this-and-that") 30 | fmt.Println(enText) 31 | 32 | // 德文 33 | deText := slug.MakeLang("Diese & Dass", "de") 34 | assert.Equal(t, deText, "diese-und-dass") 35 | fmt.Println(deText) 36 | 37 | // 保持大小写 38 | slug.Lowercase = false 39 | deUppercaseText := slug.MakeLang("Diese & Dass", "de") 40 | assert.Equal(t, deUppercaseText, "Diese-und-Dass") 41 | fmt.Println(deUppercaseText) 42 | 43 | // 字符替换 44 | slug.CustomSub = map[string]string{ 45 | "water": "sand", 46 | } 47 | textSub := slug.Make("water is hot") 48 | assert.Equal(t, textSub, "sand-is-hot") 49 | fmt.Println(textSub) 50 | } 51 | 52 | func TestGenerate(t *testing.T) { 53 | // 俄文 54 | text := Generate("Hellö Wörld хелло ворлд") 55 | assert.Equal(t, text, "hello-world-khello-vorld") 56 | fmt.Println(text) 57 | 58 | // 繁体中文 59 | someText := Generate("影師") 60 | assert.Equal(t, someText, "ying-shi") 61 | fmt.Println(someText) 62 | 63 | // 简体中文 64 | cnText := Generate("天下太平") 65 | assert.Equal(t, cnText, "tian-xia-tai-ping") 66 | fmt.Println(cnText) 67 | 68 | // 英文 69 | enText := GenerateEnglish("This & that") 70 | assert.Equal(t, enText, "this-and-that") 71 | fmt.Println(enText) 72 | 73 | enText = GenerateCaseSensitive("This & That") 74 | assert.Equal(t, enText, "This-and-That") 75 | fmt.Println(enText) 76 | 77 | // 德文 78 | deText := GenerateGerman("Diese & Dass") 79 | assert.Equal(t, deText, "diese-und-dass") 80 | fmt.Println(deText) 81 | 82 | slug.CustomSub = map[string]string{ 83 | "water": "sand", 84 | } 85 | textSub := Generate("water is hot") 86 | assert.Equal(t, textSub, "sand-is-hot") 87 | fmt.Println(textSub) 88 | } 89 | -------------------------------------------------------------------------------- /stringcase/stringcase.go: -------------------------------------------------------------------------------- 1 | package stringcase 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | // ToSnakeCase 把字符转换为 蛇形命名法(snake_case) 9 | func ToSnakeCase(input string) string { 10 | if input == "" { 11 | return input 12 | } 13 | if len(input) == 1 { 14 | return strings.ToLower(input) 15 | } 16 | 17 | input = strings.Replace(input, " ", "", -1) 18 | 19 | source := []rune(input) 20 | dist := strings.Builder{} 21 | dist.Grow(len(input) + len(input)/3) // avoid reallocation memory, 33% ~ 50% is recommended 22 | skipNext := false 23 | for i := 0; i < len(source); i++ { 24 | cur := source[i] 25 | switch cur { 26 | case '-', '_': 27 | dist.WriteRune('_') 28 | skipNext = true 29 | continue 30 | } 31 | if unicode.IsLower(cur) || unicode.IsDigit(cur) { 32 | dist.WriteRune(cur) 33 | continue 34 | } 35 | 36 | if i == 0 { 37 | dist.WriteRune(unicode.ToLower(cur)) 38 | continue 39 | } 40 | 41 | last := source[i-1] 42 | if (!unicode.IsLetter(last)) || unicode.IsLower(last) { 43 | if skipNext { 44 | skipNext = false 45 | } else { 46 | dist.WriteRune('_') 47 | } 48 | dist.WriteRune(unicode.ToLower(cur)) 49 | continue 50 | } 51 | // last is upper case 52 | if i < len(source)-1 { 53 | next := source[i+1] 54 | if unicode.IsLower(next) { 55 | if skipNext { 56 | skipNext = false 57 | } else { 58 | dist.WriteRune('_') 59 | } 60 | dist.WriteRune(unicode.ToLower(cur)) 61 | continue 62 | } 63 | } 64 | dist.WriteRune(unicode.ToLower(cur)) 65 | } 66 | 67 | return dist.String() 68 | } 69 | 70 | // ToPascalCase 把字符转换为 帕斯卡命名/大驼峰命名法(CamelCase) 71 | func ToPascalCase(input string) string { 72 | return toCamelCase(input, true) 73 | } 74 | 75 | // ToLowCamelCase 把字符转换为 小驼峰命名法(lowerCamelCase) 76 | func ToLowCamelCase(input string) string { 77 | return toCamelCase(input, false) 78 | } 79 | 80 | func toCamelCase(s string, initCase bool) string { 81 | s = strings.TrimSpace(s) 82 | if s == "" { 83 | return s 84 | } 85 | 86 | var uppercaseAcronym = map[string]string{} 87 | if a, ok := uppercaseAcronym[s]; ok { 88 | s = a 89 | } 90 | 91 | n := strings.Builder{} 92 | n.Grow(len(s)) 93 | capNext := initCase 94 | for i, v := range []byte(s) { 95 | vIsCap := v >= 'A' && v <= 'Z' 96 | vIsLow := v >= 'a' && v <= 'z' 97 | if capNext { 98 | if vIsLow { 99 | v += 'A' 100 | v -= 'a' 101 | } 102 | } else if i == 0 { 103 | if vIsCap { 104 | v += 'a' 105 | v -= 'A' 106 | } 107 | } 108 | if vIsCap || vIsLow { 109 | n.WriteByte(v) 110 | capNext = false 111 | } else if vIsNum := v >= '0' && v <= '9'; vIsNum { 112 | n.WriteByte(v) 113 | capNext = true 114 | } else { 115 | capNext = v == '_' || v == ' ' || v == '-' || v == '.' 116 | } 117 | } 118 | return n.String() 119 | } 120 | -------------------------------------------------------------------------------- /stringcase/stringcase_test.go: -------------------------------------------------------------------------------- 1 | package stringcase 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestToLowCamelCase(t *testing.T) { 9 | fmt.Println(ToLowCamelCase("snake_case")) 10 | fmt.Println(ToLowCamelCase("CamelCase")) 11 | fmt.Println(ToLowCamelCase("lowerCamelCase")) 12 | } 13 | 14 | func TestToPascalCase(t *testing.T) { 15 | fmt.Println(ToPascalCase("snake_case")) 16 | fmt.Println(ToPascalCase("CamelCase")) 17 | fmt.Println(ToPascalCase("lowerCamelCase")) 18 | } 19 | 20 | func TestToSnakeCase(t *testing.T) { 21 | fmt.Println(ToSnakeCase("snake_case")) 22 | fmt.Println(ToSnakeCase("CamelCase")) 23 | fmt.Println(ToSnakeCase("lowerCamelCase")) 24 | } 25 | -------------------------------------------------------------------------------- /stringutil/cryptorandomstringutils.go: -------------------------------------------------------------------------------- 1 | package stringutil 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "math" 7 | "math/big" 8 | "unicode" 9 | ) 10 | 11 | func CryptoRandomNonAlphaNumeric(count int) (string, error) { 12 | return CryptoRandomAlphaNumericCustom(count, false, false) 13 | } 14 | 15 | func CryptoRandomAscii(count int) (string, error) { 16 | return CryptoRandom(count, 32, 127, false, false) 17 | } 18 | 19 | func CryptoRandomNumeric(count int) (string, error) { 20 | return CryptoRandom(count, 0, 0, false, true) 21 | } 22 | 23 | func CryptoRandomAlphabetic(count int) (string, error) { 24 | return CryptoRandom(count, 0, 0, true, false) 25 | } 26 | 27 | func CryptoRandomAlphaNumeric(count int) (string, error) { 28 | return CryptoRandom(count, 0, 0, true, true) 29 | } 30 | 31 | func CryptoRandomAlphaNumericCustom(count int, letters bool, numbers bool) (string, error) { 32 | return CryptoRandom(count, 0, 0, letters, numbers) 33 | } 34 | 35 | func CryptoRandom(count int, start int, end int, letters bool, numbers bool, chars ...rune) (string, error) { 36 | if count == 0 { 37 | return "", nil 38 | } else if count < 0 { 39 | err := fmt.Errorf("randomstringutils illegal argument: Requested random string length %v is less than 0", count) // equiv to err := errors.New("...") 40 | return "", err 41 | } 42 | if chars != nil && len(chars) == 0 { 43 | err := fmt.Errorf("randomstringutils illegal argument: The chars array must not be empty") 44 | return "", err 45 | } 46 | 47 | if start == 0 && end == 0 { 48 | if chars != nil { 49 | end = len(chars) 50 | } else { 51 | if !letters && !numbers { 52 | end = math.MaxInt32 53 | } else { 54 | end = 'z' + 1 55 | start = ' ' 56 | } 57 | } 58 | } else { 59 | if end <= start { 60 | err := fmt.Errorf("randomstringutils illegal argument: Parameter end (%v) must be greater than start (%v)", end, start) 61 | return "", err 62 | } 63 | 64 | if chars != nil && end > len(chars) { 65 | err := fmt.Errorf("randomstringutils illegal argument: Parameter end (%v) cannot be greater than len(chars) (%v)", end, len(chars)) 66 | return "", err 67 | } 68 | } 69 | 70 | buffer := make([]rune, count) 71 | gap := end - start 72 | 73 | // high-surrogates range, (\uD800-\uDBFF) = 55296 - 56319 74 | // low-surrogates range, (\uDC00-\uDFFF) = 56320 - 57343 75 | 76 | for count != 0 { 77 | count-- 78 | var ch rune 79 | if chars == nil { 80 | ch = rune(getCryptoRandomInt(gap) + int64(start)) 81 | } else { 82 | ch = chars[getCryptoRandomInt(gap)+int64(start)] 83 | } 84 | 85 | if letters && unicode.IsLetter(ch) || numbers && unicode.IsDigit(ch) || !letters && !numbers { 86 | if ch >= 56320 && ch <= 57343 { // low surrogate range 87 | if count == 0 { 88 | count++ 89 | } else { 90 | // Insert low surrogate 91 | buffer[count] = ch 92 | count-- 93 | // Insert high surrogate 94 | buffer[count] = rune(55296 + getCryptoRandomInt(128)) 95 | } 96 | } else if ch >= 55296 && ch <= 56191 { // High surrogates range (Partial) 97 | if count == 0 { 98 | count++ 99 | } else { 100 | // Insert low surrogate 101 | buffer[count] = rune(56320 + getCryptoRandomInt(128)) 102 | count-- 103 | // Insert high surrogate 104 | buffer[count] = ch 105 | } 106 | } else if ch >= 56192 && ch <= 56319 { 107 | // private high surrogate, skip it 108 | count++ 109 | } else { 110 | // not one of the surrogates* 111 | buffer[count] = ch 112 | } 113 | } else { 114 | count++ 115 | } 116 | } 117 | return string(buffer), nil 118 | } 119 | 120 | func getCryptoRandomInt(count int) int64 { 121 | nBig, err := rand.Int(rand.Reader, big.NewInt(int64(count))) 122 | if err != nil { 123 | panic(err) 124 | } 125 | return nBig.Int64() 126 | } 127 | -------------------------------------------------------------------------------- /stringutil/cryptorandomstringutils_test.go: -------------------------------------------------------------------------------- 1 | package stringutil 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "testing" 7 | "unicode/utf8" 8 | ) 9 | 10 | func TestCryptoRandomNonAlphaNumeric(t *testing.T) { 11 | // If asked for a string 0 characters long, CryptoRandomNonAlphaNumeric should provide an empty string. 12 | if x, _ := CryptoRandomNonAlphaNumeric(0); utf8.RuneCountInString(x) != 0 { 13 | t.Errorf("String should be 0 characters; string was %v characters", utf8.RuneCountInString(x)) 14 | } 15 | 16 | // Test CryptoRandomNonAlphaNumeric's ability to generate strings 1 through 100 characters in length. 17 | for i := 1; i < 101; i++ { 18 | if x, _ := CryptoRandomNonAlphaNumeric(i); utf8.RuneCountInString(x) != i { 19 | t.Errorf("String should be %v characters; string was %v characters", i, utf8.RuneCountInString(x)) 20 | } 21 | } 22 | } 23 | 24 | func TestCryptoRandomAscii(t *testing.T) { 25 | // If asked for a string 0 characters long, CryptoRandomAscii should provide an empty string. 26 | if x, _ := CryptoRandomAscii(0); len(x) != 0 { 27 | t.Errorf("String should be 0 characters; string was %v characters", len(x)) 28 | } 29 | 30 | // Test CryptoRandomAscii's ability to generate strings 1 through 100 characters in length. 31 | for i := 1; i < 101; i++ { 32 | if x, _ := CryptoRandomAscii(i); len(x) != i { 33 | t.Errorf("String should be %v characters; string was %v characters", i, len(x)) 34 | } 35 | } 36 | } 37 | 38 | func TestCryptoRandomNumeric(t *testing.T) { 39 | // If asked for a string 0 characters long, CryptoRandomNumeric should provide an empty string. 40 | if x, _ := CryptoRandomNumeric(0); len(x) != 0 { 41 | t.Errorf("String should be 0 characters; string was %v characters", len(x)) 42 | } 43 | 44 | // Test CryptoRandomNumeric's ability to generate strings 1 through 100 characters in length. 45 | for i := 1; i < 101; i++ { 46 | if x, _ := CryptoRandomNumeric(i); len(x) != i { 47 | t.Errorf("String should be %v characters; string was %v characters", i, len(x)) 48 | } 49 | } 50 | } 51 | 52 | func TestCryptoRandomAlphabetic(t *testing.T) { 53 | // If asked for a string 0 characters long, CryptoRandomAlphabetic should provide an empty string. 54 | if x, _ := CryptoRandomAlphabetic(0); len(x) != 0 { 55 | t.Errorf("String should be 0 characters; string was %v characters", len(x)) 56 | } 57 | 58 | // Test CryptoRandomAlphabetic's ability to generate strings 1 through 100 characters in length. 59 | for i := 1; i < 101; i++ { 60 | if x, _ := CryptoRandomAlphabetic(i); len(x) != i { 61 | t.Errorf("String should be %v characters; string was %v characters", i, len(x)) 62 | } 63 | } 64 | } 65 | 66 | func TestCryptoRandomAlphaNumeric(t *testing.T) { 67 | // If asked for a string 0 characters long, CryptoRandomAlphaNumeric should provide an empty string. 68 | if x, _ := CryptoRandomAlphaNumeric(0); len(x) != 0 { 69 | t.Errorf("String should be 0 characters; string was %v characters", len(x)) 70 | } 71 | 72 | // Test CryptoRandomAlphaNumeric's ability to generate strings 1 through 100 characters in length. 73 | for i := 1; i < 101; i++ { 74 | if x, _ := CryptoRandomAlphaNumeric(i); len(x) != i { 75 | t.Errorf("String should be %v characters; string was %v characters", i, len(x)) 76 | } 77 | } 78 | } 79 | 80 | func TestCryptoRandAlphaNumeric_FuzzOnlyNumeric(t *testing.T) { 81 | 82 | // Testing for a reported regression in which some versions produced 83 | // a predictably small set of chars. 84 | iters := 1000 85 | charlen := 0 86 | for i := 0; i < 16; i++ { 87 | numOnly := 0 88 | charlen++ 89 | for i := 0; i < iters; i++ { 90 | out, err := CryptoRandomAlphaNumeric(charlen) 91 | if err != nil { 92 | t.Fatal("func failed to produce a random thinger") 93 | } 94 | if _, err := strconv.Atoi(out); err == nil { 95 | numOnly++ 96 | } 97 | 98 | m, err := regexp.MatchString("^[0-9a-zA-Z]+$", out) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | if !m { 103 | t.Fatal("Character is not alphanum") 104 | } 105 | } 106 | 107 | if numOnly == iters { 108 | t.Fatalf("Got %d numeric-only random sequences", numOnly) 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /stringutil/randomstringutils.go: -------------------------------------------------------------------------------- 1 | package stringutil 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "time" 8 | "unicode" 9 | ) 10 | 11 | var RANDOM = rand.New(rand.NewSource(time.Now().UnixNano())) 12 | 13 | func RandomNonAlphaNumeric(count int) (string, error) { 14 | return RandomAlphaNumericCustom(count, false, false) 15 | } 16 | 17 | func RandomAscii(count int) (string, error) { 18 | return Random(count, 32, 127, false, false) 19 | } 20 | 21 | func RandomNumeric(count int) (string, error) { 22 | return Random(count, 0, 0, false, true) 23 | } 24 | 25 | func RandomAlphabetic(count int) (string, error) { 26 | return Random(count, 0, 0, true, false) 27 | } 28 | 29 | func RandomAlphaNumeric(count int) (string, error) { 30 | return Random(count, 0, 0, true, true) 31 | } 32 | 33 | func RandomAlphaNumericCustom(count int, letters bool, numbers bool) (string, error) { 34 | return Random(count, 0, 0, letters, numbers) 35 | } 36 | 37 | func Random(count int, start int, end int, letters bool, numbers bool, chars ...rune) (string, error) { 38 | return RandomSeed(count, start, end, letters, numbers, chars, RANDOM) 39 | } 40 | 41 | func RandomSeed(count int, start int, end int, letters bool, numbers bool, chars []rune, random *rand.Rand) (string, error) { 42 | 43 | if count == 0 { 44 | return "", nil 45 | } else if count < 0 { 46 | err := fmt.Errorf("randomstringutils illegal argument: Requested random string length %v is less than 0", count) // equiv to err := errors.New("...") 47 | return "", err 48 | } 49 | if chars != nil && len(chars) == 0 { 50 | err := fmt.Errorf("randomstringutils illegal argument: The chars array must not be empty") 51 | return "", err 52 | } 53 | 54 | if start == 0 && end == 0 { 55 | if chars != nil { 56 | end = len(chars) 57 | } else { 58 | if !letters && !numbers { 59 | end = math.MaxInt32 60 | } else { 61 | end = 'z' + 1 62 | start = ' ' 63 | } 64 | } 65 | } else { 66 | if end <= start { 67 | err := fmt.Errorf("randomstringutils illegal argument: Parameter end (%v) must be greater than start (%v)", end, start) 68 | return "", err 69 | } 70 | 71 | if chars != nil && end > len(chars) { 72 | err := fmt.Errorf("randomstringutils illegal argument: Parameter end (%v) cannot be greater than len(chars) (%v)", end, len(chars)) 73 | return "", err 74 | } 75 | } 76 | 77 | buffer := make([]rune, count) 78 | gap := end - start 79 | 80 | // high-surrogates range, (\uD800-\uDBFF) = 55296 - 56319 81 | // low-surrogates range, (\uDC00-\uDFFF) = 56320 - 57343 82 | 83 | for count != 0 { 84 | count-- 85 | var ch rune 86 | if chars == nil { 87 | ch = rune(random.Intn(gap) + start) 88 | } else { 89 | ch = chars[random.Intn(gap)+start] 90 | } 91 | 92 | if letters && unicode.IsLetter(ch) || numbers && unicode.IsDigit(ch) || !letters && !numbers { 93 | if ch >= 56320 && ch <= 57343 { // low surrogate range 94 | if count == 0 { 95 | count++ 96 | } else { 97 | // Insert low surrogate 98 | buffer[count] = ch 99 | count-- 100 | // Insert high surrogate 101 | buffer[count] = rune(55296 + random.Intn(128)) 102 | } 103 | } else if ch >= 55296 && ch <= 56191 { // High surrogates range (Partial) 104 | if count == 0 { 105 | count++ 106 | } else { 107 | // Insert low surrogate 108 | buffer[count] = rune(56320 + random.Intn(128)) 109 | count-- 110 | // Insert high surrogate 111 | buffer[count] = ch 112 | } 113 | } else if ch >= 56192 && ch <= 56319 { 114 | // private high surrogate, skip it 115 | count++ 116 | } else { 117 | // not one of the surrogates* 118 | buffer[count] = ch 119 | } 120 | } else { 121 | count++ 122 | } 123 | } 124 | return string(buffer), nil 125 | } 126 | -------------------------------------------------------------------------------- /stringutil/randomstringutils_test.go: -------------------------------------------------------------------------------- 1 | package stringutil 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "regexp" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | // ****************************** TESTS ******************************************** 12 | 13 | func TestRandomSeed(t *testing.T) { 14 | 15 | // count, start, end, letters, numbers := 5, 0, 0, true, true 16 | random := rand.New(rand.NewSource(10)) 17 | out := "3ip9v" 18 | 19 | // Test 1: Simulating RandomAlphaNumeric(count int) 20 | if x, _ := RandomSeed(5, 0, 0, true, true, nil, random); x != out { 21 | t.Errorf("RandomSeed(%v, %v, %v, %v, %v, %v, %v) = %v, want %v", 5, 0, 0, true, true, nil, random, x, out) 22 | } 23 | 24 | // Test 2: Simulating RandomAlphabetic(count int) 25 | out = "MBrbj" 26 | 27 | if x, _ := RandomSeed(5, 0, 0, true, false, nil, random); x != out { 28 | t.Errorf("RandomSeed(%v, %v, %v, %v, %v, %v, %v) = %v, want %v", 5, 0, 0, true, false, nil, random, x, out) 29 | } 30 | 31 | // Test 3: Simulating RandomNumeric(count int) 32 | out = "88935" 33 | 34 | if x, _ := RandomSeed(5, 0, 0, false, true, nil, random); x != out { 35 | t.Errorf("RandomSeed(%v, %v, %v, %v, %v, %v, %v) = %v, want %v", 5, 0, 0, false, true, nil, random, x, out) 36 | } 37 | 38 | // Test 4: Simulating RandomAscii(count int) 39 | out = "H_I;E" 40 | 41 | if x, _ := RandomSeed(5, 32, 127, false, false, nil, random); x != out { 42 | t.Errorf("RandomSeed(%v, %v, %v, %v, %v, %v, %v) = %v, want %v", 5, 32, 127, false, false, nil, random, x, out) 43 | } 44 | 45 | // Test 5: Simulating RandomSeed(...) with custom chars 46 | chars := []rune{'1', '2', '3', 'a', 'b', 'c'} 47 | out = "2b2ca" 48 | 49 | if x, _ := RandomSeed(5, 0, 0, false, false, chars, random); x != out { 50 | t.Errorf("RandomSeed(%v, %v, %v, %v, %v, %v, %v) = %v, want %v", 5, 0, 0, false, false, chars, random, x, out) 51 | } 52 | 53 | } 54 | 55 | // ****************************** EXAMPLES ******************************************** 56 | 57 | func ExampleRandomSeed() { 58 | 59 | var seed int64 = 10 // If you change this seed #, the random sequence below will change 60 | random := rand.New(rand.NewSource(seed)) 61 | chars := []rune{'1', '2', '3', 'a', 'b', 'c'} 62 | 63 | rand1, _ := RandomSeed(5, 0, 0, true, true, nil, random) // RandomAlphaNumeric (Alphabets and numbers possible) 64 | rand2, _ := RandomSeed(5, 0, 0, true, false, nil, random) // RandomAlphabetic (Only alphabets) 65 | rand3, _ := RandomSeed(5, 0, 0, false, true, nil, random) // RandomNumeric (Only numbers) 66 | rand4, _ := RandomSeed(5, 32, 127, false, false, nil, random) // RandomAscii (Alphabets, numbers, and other ASCII chars) 67 | rand5, _ := RandomSeed(5, 0, 0, true, true, chars, random) // RandomSeed with custom characters 68 | 69 | fmt.Println(rand1) 70 | fmt.Println(rand2) 71 | fmt.Println(rand3) 72 | fmt.Println(rand4) 73 | fmt.Println(rand5) 74 | // Output: 75 | // 3ip9v 76 | // MBrbj 77 | // 88935 78 | // H_I;E 79 | // 2b2ca 80 | } 81 | 82 | func TestRandomAlphaNumeric(t *testing.T) { 83 | for i := 0; i < 16; i++ { 84 | out, _ := RandomAlphaNumeric(20) 85 | fmt.Println(out) 86 | } 87 | } 88 | 89 | func TestRandAlphaNumeric_FuzzOnlyNumeric(t *testing.T) { 90 | 91 | // Testing for a reported regression in which some versions produced 92 | // a predictably small set of chars. 93 | iters := 1000 94 | charlen := 0 95 | for i := 0; i < 16; i++ { 96 | numOnly := 0 97 | charlen++ 98 | for i := 0; i < iters; i++ { 99 | out, err := RandomAlphaNumeric(charlen) 100 | if err != nil { 101 | t.Fatal("func failed to produce a random thinger") 102 | } 103 | if _, err := strconv.Atoi(out); err == nil { 104 | numOnly++ 105 | } 106 | 107 | m, err := regexp.MatchString("^[0-9a-zA-Z]+$", out) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | if !m { 112 | t.Fatal("Character is not alphanum") 113 | } 114 | } 115 | 116 | if numOnly == iters { 117 | t.Fatalf("Got %d numeric-only random sequences", numOnly) 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /stringutil/replace.go: -------------------------------------------------------------------------------- 1 | package stringutil 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | // ReplaceJSONField 使用正则表达式替换 JSON 字符串中指定字段的值 9 | // fieldNames: 要替换的多个字段名,使用竖线(|)分隔(例如:"tenantId|tenant_id") 10 | // newValue: 新的值(字符串形式,将被包装在引号中) 11 | // jsonStr: 原始 JSON 字符串 12 | func ReplaceJSONField(fieldNames, newValue, jsonStr string) string { 13 | // 构建正则表达式模式 14 | // 匹配模式: ("field1"|"field2"|...): "任意值" 15 | pattern := fmt.Sprintf(`(?i)("(%s)")\s*:\s*"([^"]*)"`, fieldNames) 16 | re := regexp.MustCompile(pattern) 17 | 18 | // 替换匹配到的内容 19 | return re.ReplaceAllString(jsonStr, fmt.Sprintf(`${1}: "%s"`, newValue)) 20 | } 21 | -------------------------------------------------------------------------------- /stringutil/replace_test.go: -------------------------------------------------------------------------------- 1 | package stringutil 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestReplaceJSONField(t *testing.T) { 9 | // 测试替换单个字段 10 | jsonStr := `{"tenantId": "123", "name": "test"}` 11 | result := ReplaceJSONField("tenantId", "456", jsonStr) 12 | expected := `{"tenantId": "456", "name": "test"}` 13 | assert.Equal(t, expected, result) 14 | 15 | // 测试替换多个字段 16 | jsonStr = `{"tenantId": "123", "tenant_id": "789", "name": "test"}` 17 | result = ReplaceJSONField("tenantId|tenant_id", "456", jsonStr) 18 | expected = `{"tenantId": "456", "tenant_id": "456", "name": "test"}` 19 | assert.Equal(t, expected, result) 20 | 21 | // 测试字段不存在 22 | jsonStr = `{"name": "test"}` 23 | result = ReplaceJSONField("tenantId", "456", jsonStr) 24 | expected = `{"name": "test"}` 25 | assert.Equal(t, expected, result) 26 | 27 | // 测试空 JSON 字符串 28 | jsonStr = `` 29 | result = ReplaceJSONField("tenantId", "456", jsonStr) 30 | expected = `` 31 | assert.Equal(t, expected, result) 32 | 33 | // 测试空字段名 34 | jsonStr = `{"tenantId": "123"}` 35 | result = ReplaceJSONField("", "456", jsonStr) 36 | expected = `{"tenantId": "123"}` 37 | assert.Equal(t, expected, result) 38 | } 39 | -------------------------------------------------------------------------------- /structutil/structutils.go: -------------------------------------------------------------------------------- 1 | package structutil 2 | 3 | import "reflect" 4 | 5 | // ForEach given a struct, calls the passed in function with each visible struct field's name, value and tag. 6 | func ForEach[T any](structInstance T, function func(key string, value any, tag reflect.StructTag)) { 7 | typeOf := reflect.TypeOf(structInstance) 8 | valueOf := reflect.ValueOf(structInstance) 9 | fields := reflect.VisibleFields(typeOf) 10 | 11 | for _, field := range fields { 12 | value := valueOf.FieldByName(field.Name) 13 | function(field.Name, value.Interface(), field.Tag) 14 | } 15 | } 16 | 17 | // ToMap given a struct, converts it to a map[string]any. 18 | // This function also takes struct tag names as optional parameters - if passed in, the struct tags will be used to remap or omit values. 19 | func ToMap[T any](structInstance T, structTags ...string) map[string]any { 20 | output := make(map[string]any, 0) 21 | 22 | ForEach(structInstance, func(key string, value any, tag reflect.StructTag) { 23 | if tag != "" { 24 | for _, s := range structTags { 25 | if tagValue, isPresent := tag.Lookup(s); isPresent && tagValue != "" { 26 | key = tagValue 27 | break 28 | } 29 | } 30 | } 31 | if key != "-" { 32 | output[key] = value 33 | } 34 | }) 35 | 36 | return output 37 | } 38 | -------------------------------------------------------------------------------- /structutil/structutils_test.go: -------------------------------------------------------------------------------- 1 | package structutil_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/tx7do/go-utils/structutil" 9 | ) 10 | 11 | type TestStruct struct { 12 | First string 13 | Second int 14 | Third bool `struct:"third"` 15 | } 16 | 17 | func TestForEach(t *testing.T) { 18 | instance := TestStruct{ 19 | "moishe", 20 | 22, 21 | true, 22 | } 23 | structutil.ForEach(instance, func(key string, value any, tag reflect.StructTag) { 24 | switch key { 25 | case "First": 26 | assert.Equal(t, "moishe", value.(string)) 27 | assert.Zero(t, tag) 28 | case "Second": 29 | assert.Equal(t, 22, value.(int)) 30 | assert.Zero(t, tag) 31 | case "Third": 32 | assert.Equal(t, true, value.(bool)) 33 | assert.Equal(t, "third", tag.Get("struct")) 34 | } 35 | }) 36 | } 37 | 38 | func TestToMap(t *testing.T) { 39 | instance := TestStruct{ 40 | "moishe", 41 | 22, 42 | true, 43 | } 44 | assert.Equal(t, map[string]any{ 45 | "First": "moishe", 46 | "Second": 22, 47 | "Third": true, 48 | }, structutil.ToMap(instance)) 49 | 50 | assert.Equal(t, map[string]any{ 51 | "First": "moishe", 52 | "Second": 22, 53 | "third": true, 54 | }, structutil.ToMap(instance, "struct")) 55 | } 56 | -------------------------------------------------------------------------------- /tag.bat: -------------------------------------------------------------------------------- 1 | git tag v1.1.28 2 | 3 | git tag bank_card/v1.1.5 4 | git tag geoip/v1.1.5 5 | git tag translator/v1.1.2 6 | git tag copierutil/v0.0.5 7 | git tag jwtutil/v0.0.2 8 | git tag id/v0.0.2 9 | git tag slug/v0.0.1 10 | 11 | git tag entgo/v1.1.31 12 | git tag gorm/v1.1.6 13 | 14 | git push origin --tags 15 | -------------------------------------------------------------------------------- /timeutil/consts.go: -------------------------------------------------------------------------------- 1 | package timeutil 2 | 3 | import "time" 4 | 5 | const ( 6 | DateLayout = "2006-01-02" 7 | ClockLayout = "15:04:05" 8 | TimeLayout = DateLayout + " " + ClockLayout 9 | 10 | DefaultTimeLocationName = "Asia/Shanghai" 11 | ) 12 | 13 | // More predefined layouts for use in Time.Format and time.Parse. 14 | const ( 15 | DT14 = "20060102150405" 16 | DT8 = "20060102" 17 | DT8MDY = "01022006" 18 | DT6 = "200601" 19 | MonthDay = "1/2" 20 | DIN5008FullDate = "02.01.2006" // German DIN 5008 standard 21 | DIN5008Date = "02.01.06" 22 | RFC3339FullDate = time.DateOnly 23 | RFC3339Milli = "2006-01-02T15:04:05.999Z07:00" 24 | RFC3339Dash = "2006-01-02T15-04-05Z07-00" 25 | ISO8601 = "2006-01-02T15:04:05Z0700" 26 | ISO8601TZHour = "2006-01-02T15:04:05Z07" 27 | ISO8601NoTZ = "2006-01-02T15:04:05" 28 | ISO8601MilliNoTZ = "2006-01-02T15:04:05.999" 29 | ISO8601Milli = "2006-01-02T15:04:05.999Z0700" 30 | ISO8601CompactZ = "20060102T150405Z0700" 31 | ISO8601CompactNoTZ = "20060102T150405" 32 | ISO8601YM = "2006-01" 33 | ISO9075 = time.DateTime // ISO/IEC 9075 used by MySQL, BigQuery, etc. 34 | ISO9075MicroTZ = "2006-01-02 15:04:05.999999-07" // ISO/IEC 9075 used by PostgreSQL 35 | RFC5322 = "Mon, 2 Jan 2006 15:04:05 -0700" // RFC5322 = "Mon Jan 02 15:04:05 -0700 2006" 36 | SQLTimestamp = ISO9075 37 | SQLTimestampMinutes = "2006-01-02 15:04" 38 | Ruby = "2006-01-02 15:04:05 -0700" // Ruby Time.now.to_s 39 | InsightlyAPIQuery = "_1/_2/2006 _3:04:05 PM" 40 | DateMDY = "1/2/2006" // an underscore results in a space. 41 | DateMDYSlash = "01/02/2006" 42 | DateDMYDash = "_2-01-2006" // Jira XML Date format 43 | DateDMYHM2 = "02:01:06 15:04" // GMT time in format dd:mm:yy hh:mm 44 | DateYMD = RFC3339FullDate 45 | DateTextUS = "January 2, 2006" 46 | DateTextUSAbbr3 = "Jan 2, 2006" 47 | DateTextEU = "2 January 2006" 48 | DateTextEUAbbr3 = "2 Jan 2006" 49 | MonthAbbrYear = "Jan 2006" 50 | MonthYear = "January 2006" 51 | ) 52 | 53 | const ( 54 | RFC3339Min = "0000-01-01T00:00:00Z" 55 | RFC3339Max = "9999-12-31T23:59:59Z" 56 | RFC3339Zero = "0001-01-01T00:00:00Z" // Golang zero value 57 | RFC3339ZeroUnix = "1970-01-01T00:00:00Z" 58 | RFC3339YMDZeroUnix = int64(-62135596800) 59 | ) 60 | 61 | var ReferenceTimeValue time.Time = time.Date(2006, 1, 2, 15, 4, 5, 999999999, time.FixedZone("MST", -7*60*60)) 62 | -------------------------------------------------------------------------------- /timeutil/diff.go: -------------------------------------------------------------------------------- 1 | package timeutil 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // DayDifferenceHours 两天之间相差了多少小时 9 | func DayDifferenceHours(startDate, endDate string) float64 { 10 | startTime, _ := time.Parse(DateLayout, startDate) 11 | endTime, _ := time.Parse(DateLayout, endDate) 12 | 13 | duration := endTime.Sub(startTime) 14 | 15 | return duration.Hours() 16 | } 17 | 18 | // StringDifferenceDays 两天之间相差了多少天 19 | func StringDifferenceDays(startDate, endDate string) int { 20 | hours := DayDifferenceHours(startDate, endDate) 21 | if hours == 0 { 22 | return 0 23 | } 24 | return int(math.Ceil(hours / 24)) 25 | } 26 | 27 | func DayTimeDifferenceHours(startDate, endDate time.Time) float64 { 28 | startTime := time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, time.Local) 29 | endTime := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, time.Local) 30 | 31 | duration := endTime.Sub(startTime) 32 | 33 | return duration.Hours() 34 | } 35 | 36 | // TimeDifferenceDays 两天之间相差了多少天 37 | func TimeDifferenceDays(startDate, endDate time.Time) int { 38 | hours := DayTimeDifferenceHours(startDate, endDate) 39 | if hours == 0 { 40 | return 0 41 | } 42 | return int(math.Ceil(hours / 24)) 43 | } 44 | 45 | func DaySecondsDifferenceHours(startSecond, endSecond int64) float64 { 46 | startTime := time.Unix(startSecond, 0) 47 | endTime := time.Unix(endSecond, 0) 48 | 49 | duration := endTime.Sub(startTime) 50 | 51 | return duration.Hours() 52 | } 53 | 54 | // SecondsDifferenceDays 两天之间相差了多少天 55 | func SecondsDifferenceDays(startSecond, endSecond int64) int { 56 | hours := DaySecondsDifferenceHours(startSecond, endSecond) 57 | if hours == 0 { 58 | return 0 59 | } 60 | return int(math.Ceil(hours / 24)) 61 | } 62 | -------------------------------------------------------------------------------- /timeutil/diff_test.go: -------------------------------------------------------------------------------- 1 | package timeutil 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func parseDate(str string) time.Time { 11 | t, _ := time.Parse(DateLayout, str) 12 | return t 13 | } 14 | 15 | func toSecond(str string) int64 { 16 | t, _ := time.Parse(DateLayout, str) 17 | return t.Unix() 18 | } 19 | 20 | func TestDifferenceDays(t *testing.T) { 21 | assert.Equal(t, StringDifferenceDays("2017-09-01", "2017-09-01"), 0) 22 | assert.Equal(t, StringDifferenceDays("2017-09-01", "2017-09-02"), 1) 23 | assert.Equal(t, StringDifferenceDays("2017-09-01", "2017-09-03"), 2) 24 | assert.Equal(t, StringDifferenceDays("2017-09-01", "2017-09-04"), 3) 25 | 26 | assert.Equal(t, StringDifferenceDays("2017-09-01", "2018-03-11"), 191) 27 | 28 | assert.True(t, (StringDifferenceDays("2017-09-01", "2017-09-01")) == 0) 29 | assert.True(t, (StringDifferenceDays("2017-09-01", "2017-09-02"))%1 == 0) 30 | assert.True(t, (StringDifferenceDays("2017-09-01", "2017-09-03"))%2 == 0) 31 | } 32 | 33 | func TestTimeDifferenceDays(t *testing.T) { 34 | assert.Equal(t, TimeDifferenceDays(parseDate("2017-09-01"), parseDate("2017-09-01")), 0) 35 | assert.Equal(t, TimeDifferenceDays(parseDate("2017-09-01"), parseDate("2017-09-02")), 1) 36 | assert.Equal(t, TimeDifferenceDays(parseDate("2017-09-01"), parseDate("2017-09-03")), 2) 37 | assert.Equal(t, TimeDifferenceDays(parseDate("2017-09-01"), parseDate("2017-09-04")), 3) 38 | 39 | assert.Equal(t, TimeDifferenceDays(parseDate("2017-09-01"), parseDate("2018-03-11")), 191) 40 | 41 | assert.True(t, (TimeDifferenceDays(parseDate("2017-09-01"), parseDate("2017-09-01"))) == 0) 42 | assert.True(t, (TimeDifferenceDays(parseDate("2017-09-01"), parseDate("2017-09-02")))%1 == 0) 43 | assert.True(t, (TimeDifferenceDays(parseDate("2017-09-01"), parseDate("2017-09-03")))%2 == 0) 44 | } 45 | 46 | func TestSecondsDifferenceDays(t *testing.T) { 47 | assert.Equal(t, SecondsDifferenceDays(toSecond("2017-09-01"), toSecond("2017-09-01")), 0) 48 | assert.Equal(t, SecondsDifferenceDays(toSecond("2017-09-01"), toSecond("2017-09-02")), 1) 49 | assert.Equal(t, SecondsDifferenceDays(toSecond("2017-09-01"), toSecond("2017-09-03")), 2) 50 | assert.Equal(t, SecondsDifferenceDays(toSecond("2017-09-01"), toSecond("2017-09-04")), 3) 51 | 52 | assert.Equal(t, SecondsDifferenceDays(toSecond("2017-09-01"), toSecond("2018-03-11")), 191) 53 | 54 | assert.True(t, (SecondsDifferenceDays(toSecond("2017-09-01"), toSecond("2017-09-01"))) == 0) 55 | assert.True(t, (SecondsDifferenceDays(toSecond("2017-09-01"), toSecond("2017-09-02")))%1 == 0) 56 | assert.True(t, (SecondsDifferenceDays(toSecond("2017-09-01"), toSecond("2017-09-03")))%2 == 0) 57 | } 58 | -------------------------------------------------------------------------------- /timeutil/format_test.go: -------------------------------------------------------------------------------- 1 | package timeutil 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var dmyhm2ParseTests = []struct { 10 | v string 11 | want string 12 | }{ 13 | {"02:01:06 15:04", "2006-01-02T15:04:00Z"}, 14 | } 15 | 16 | // TestDMYHM2ParseTime ensures timeutil.DateDMYHM2 is parsed to GMT timezone. 17 | func TestDMYHM2ParseTime(t *testing.T) { 18 | for _, tt := range dmyhm2ParseTests { 19 | got, err := FromTo(tt.v, DateDMYHM2, time.RFC3339) 20 | if err != nil { 21 | t.Errorf("time.Parse(DateDMYHM2) Error: with %v, want %v, err %v", tt.v, tt.want, err) 22 | } 23 | if got != tt.want { 24 | t.Errorf("time.Parse(\"%v\", DateDMYHM2) Mismatch: want %v, got %v", tt.v, tt.want, got) 25 | } 26 | } 27 | } 28 | 29 | var rfc3339YMDTimeTests = []struct { 30 | v string 31 | want string 32 | }{ 33 | {`{"MyTime":"2001-02-03"}`, `{"MyTime":"2001-02-03"}`}, 34 | {`{"MyTime":"0001-01-01"}`, `{"MyTime":"0001-01-01"}`}, 35 | {`{"MyTime":""}`, `{"MyTime":"0001-01-01"}`}, 36 | {`{}`, `{"MyTime":"0001-01-01"}`}} 37 | 38 | type myStruct struct { 39 | MyTime RFC3339YMDTime 40 | } 41 | 42 | func TestRfc3339YMDTime(t *testing.T) { 43 | for _, tt := range rfc3339YMDTimeTests { 44 | my := myStruct{} 45 | //fmt.Println(tt.v) 46 | err := json.Unmarshal([]byte(tt.v), &my) 47 | if err != nil { 48 | t.Errorf("Rfc3339YMDTime.Unmarshal: with %v, want %v, err %v", tt.v, tt.want, err) 49 | } 50 | 51 | bytes, err := json.Marshal(my) 52 | if err != nil { 53 | t.Errorf("Rfc3339YMDTime.Marshal: with %v, want %v, err %v", tt.v, tt.want, err) 54 | } 55 | 56 | got := string(bytes) 57 | 58 | if got != tt.want { 59 | t.Errorf("Rfc3339YMDTime(%v): want %v, got %v", tt.v, tt.want, got) 60 | } 61 | } 62 | } 63 | 64 | var offsetFormatTests = []struct { 65 | input int 66 | useColon bool 67 | useZ bool 68 | want string 69 | }{ 70 | {0, false, false, "+0000"}, 71 | {0, true, false, "+00:00"}, 72 | {0, true, true, "Z"}, 73 | {400, false, false, "+0400"}, 74 | {-400, false, false, "-0400"}, 75 | {530, false, false, "+0530"}, 76 | {-530, false, false, "-0530"}, 77 | {700, true, false, "+07:00"}, 78 | {-700, true, false, "-07:00"}, 79 | {845, true, false, "+08:45"}, 80 | {-845, true, false, "-08:45"}, 81 | } 82 | 83 | func TestOffsetFormat(t *testing.T) { 84 | for _, tt := range offsetFormatTests { 85 | got := OffsetFormat(tt.input, tt.useColon, tt.useZ) 86 | if got != tt.want { 87 | t.Errorf("time.OffsetFormat(\"%v\",%v,%v) Mismatch: want [%v], got [%v]", tt.input, tt.useColon, tt.useZ, tt.want, got) 88 | } 89 | } 90 | } 91 | 92 | var isDTXTests = []struct { 93 | v int 94 | want string 95 | }{ 96 | {2004, LayoutNameDT4}, 97 | {200401, LayoutNameDT6}, 98 | } 99 | 100 | func TestIsDTX(t *testing.T) { 101 | for _, tt := range isDTXTests { 102 | got, err := IsDTX(tt.v) 103 | if err != nil { 104 | t.Errorf("time.IsDTX(%d) Error: want (%s), err (%v)", tt.v, tt.want, err) 105 | } 106 | if got != tt.want { 107 | t.Errorf("time.IsDTX(%d) Mismatch: want (%s), got (%s)", tt.v, tt.want, got) 108 | } 109 | } 110 | } 111 | 112 | var formatsTests = []struct { 113 | v string 114 | format string 115 | want string 116 | }{ 117 | {"2023-06-18T00:00:00Z", DateTextUS, "June 18, 2023"}, 118 | {"2023-06-18T00:00:00Z", DateTextUSAbbr3, "Jun 18, 2023"}, 119 | } 120 | 121 | func TestFormats(t *testing.T) { 122 | for _, tt := range formatsTests { 123 | dt, err := time.Parse(time.RFC3339, tt.v) 124 | if err != nil { 125 | t.Errorf("time.Parse(time.RFC3339, %s) Error: err (%s)", tt.v, err.Error()) 126 | } 127 | got := dt.Format(tt.format) 128 | if got != tt.want { 129 | t.Errorf("time.Format(%s) dt (%s) Mismatch: want (%s), got (%s)", tt.format, tt.v, tt.want, got) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /timeutil/range_test.go: -------------------------------------------------------------------------------- 1 | package timeutil 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestGetCurrentTimeRangeDateString(t *testing.T) { 9 | fmt.Println(GetTodayRangeDateString()) 10 | fmt.Println(GetCurrentMonthRangeDateString()) 11 | fmt.Println(GetCurrentYearRangeDateString()) 12 | 13 | fmt.Println(GetYesterdayRangeDateString()) 14 | fmt.Println(GetLastMonthRangeDateString()) 15 | fmt.Println(GetLastYearRangeDateString()) 16 | } 17 | 18 | func TestGetCurrentRangeTime(t *testing.T) { 19 | fmt.Println(GetTodayRangeTime()) 20 | fmt.Println(GetCurrentMonthRangeTime()) 21 | fmt.Println(GetCurrentYearRangeTime()) 22 | 23 | fmt.Println(GetYesterdayRangeTime()) 24 | fmt.Println(GetLastMonthRangeTime()) 25 | fmt.Println(GetLastYearRangeTime()) 26 | } 27 | 28 | func TestGetCurrentTimeRangeTimeString(t *testing.T) { 29 | fmt.Println(GetTodayRangeTimeString()) 30 | fmt.Println(GetCurrentMonthRangeTimeString()) 31 | fmt.Println(GetCurrentYearRangeTimeString()) 32 | 33 | fmt.Println(GetYesterdayRangeTimeString()) 34 | fmt.Println(GetLastMonthRangeTimeString()) 35 | fmt.Println(GetLastYearRangeTimeString()) 36 | } 37 | -------------------------------------------------------------------------------- /trans/to.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package trans 5 | 6 | // Ptr returns a pointer to the provided value. 7 | func Ptr[T any](v T) *T { 8 | return &v 9 | } 10 | 11 | // SliceOfPtrs returns a slice of *T from the specified values. 12 | func SliceOfPtrs[T any](vv ...T) []*T { 13 | slc := make([]*T, len(vv)) 14 | for i := range vv { 15 | slc[i] = Ptr(vv[i]) 16 | } 17 | return slc 18 | } 19 | -------------------------------------------------------------------------------- /trans/to_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package trans 5 | 6 | import "testing" 7 | 8 | func TestPtr(t *testing.T) { 9 | b := true 10 | pb := Ptr(b) 11 | if pb == nil { 12 | t.Fatal("unexpected nil conversion") 13 | } 14 | if *pb != b { 15 | t.Fatalf("got %v, want %v", *pb, b) 16 | } 17 | } 18 | 19 | func TestSliceOfPtrs(t *testing.T) { 20 | arr := SliceOfPtrs[int]() 21 | if len(arr) != 0 { 22 | t.Fatal("expected zero length") 23 | } 24 | arr = SliceOfPtrs(1, 2, 3, 4, 5) 25 | for i, v := range arr { 26 | if *v != i+1 { 27 | t.Fatal("values don't match") 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /translator/README.md: -------------------------------------------------------------------------------- 1 | # 翻译器 2 | 3 | ## 目标语言 4 | 5 | 1. 阿尔巴尼亚语: `sq` 6 | 2. 阿拉伯语: `ar` 7 | 3. 阿姆哈拉语: `am` 8 | 4. 阿塞拜疆语: `az` 9 | 5. 爱尔兰语: `ga` 10 | 6. 爱沙尼亚语: `et` 11 | 7. 奥利亚语: `or` 12 | 8. 巴斯克语: `eu` 13 | 9. 白俄罗斯语: `be` 14 | 10. 保加利亚语: `bg` 15 | 11. 冰岛语: `is` 16 | 12. 波兰语: `pl` 17 | 13. 波斯尼亚语: `bs` 18 | 14. 波斯语: `fa` 19 | 15. 布尔语(南非荷兰语): `af` 20 | 16. 鞑靼语: `tt` 21 | 17. 丹麦语: `da` 22 | 18. 德语: `de` 23 | 19. 俄语: `ru` 24 | 20. 法语: `fr` 25 | 21. 菲律宾语: `tl` 26 | 22. 芬兰语: `fi` 27 | 23. 弗里西语: `fy` 28 | 24. 高棉语: `km` 29 | 25. 格鲁吉亚语: `ka` 30 | 26. 古吉拉特语: `gu` 31 | 27. 哈萨克语: `kk` 32 | 28. 海地克里奥尔语: `ht` 33 | 29. 韩语: `ko` 34 | 30. 豪萨语: `ha` 35 | 31. 荷兰语: `nl` 36 | 32. 吉尔吉斯语: `ky` 37 | 33. 加利西亚语: `gl` 38 | 34. 加泰罗尼亚语: `ca` 39 | 35. 捷克语: `cs` 40 | 36. 卡纳达语: `kn` 41 | 37. 科西嘉语: `co` 42 | 38. 克罗地亚语: `hr` 43 | 39. 库尔德语: `ku` 44 | 40. 拉丁语: `la` 45 | 41. 拉脱维亚语: `lv` 46 | 42. 老挝语: `lo` 47 | 43. 立陶宛语: `lt` 48 | 44. 卢森堡语: `lb` 49 | 45. 卢旺达语: `rw` 50 | 46. 罗马尼亚语: `ro` 51 | 47. 马尔加什语: `mg` 52 | 48. 马耳他语: `mt` 53 | 49. 马拉地语: `mr` 54 | 50. 马拉雅拉姆语: `ml` 55 | 51. 马来语: `ms` 56 | 52. 马其顿语: `mk` 57 | 53. 毛利语: `mi` 58 | 54. 蒙古语: `mn` 59 | 55. 孟加拉语: `bn` 60 | 56. 缅甸语: `my` 61 | 57. 苗语: `hmn` 62 | 58. 南非科萨语: `xh` 63 | 59. 南非祖鲁语: `zu` 64 | 60. 尼泊尔语: `ne` 65 | 61. 挪威语: `no` 66 | 62. 旁遮普语: `pa` 67 | 63. 葡萄牙语: `pt` 68 | 64. 普什图语: `ps` 69 | 65. 齐切瓦语: `ny` 70 | 66. 日语: `ja` 71 | 67. 瑞典语: `sv` 72 | 68. 萨摩亚语: `sm` 73 | 69. 塞尔维亚语: `sr` 74 | 70. 塞索托语: `st` 75 | 71. 僧伽罗语: `si` 76 | 72. 世界语: `eo` 77 | 73. 斯洛伐克语: `sk` 78 | 74. 斯洛文尼亚语: `sl` 79 | 75. 斯瓦希里语: `sw` 80 | 76. 苏格兰盖尔语: `gd` 81 | 77. 宿务语: `ceb` 82 | 78. 索马里语: `so` 83 | 79. 塔吉克语: `tg` 84 | 80. 泰卢固语: `te` 85 | 81. 泰米尔语: `ta` 86 | 82. 泰语: `th` 87 | 83. 土耳其语: `tr` 88 | 84. 土库曼语: `tk` 89 | 85. 威尔士语: `cy` 90 | 86. 维吾尔语: `ug` 91 | 87. 乌尔都语: `ur` 92 | 88. 乌克兰语: `uk` 93 | 89. 乌兹别克语: `uz` 94 | 90. 西班牙语: `es` 95 | 91. 希伯来语: `iw` 96 | 92. 希腊语: `el` 97 | 93. 夏威夷语: `haw` 98 | 94. 信德语: `sd` 99 | 95. 匈牙利语: `hu` 100 | 96. 修纳语: `sn` 101 | 97. 亚美尼亚语: `hy` 102 | 98. 伊博语: `ig` 103 | 99. 意大利语: `it` 104 | 100. 意第绪语: `yi` 105 | 101. 印地语: `hi` 106 | 102. 印尼巽他语: `su` 107 | 103. 印尼语: `id` 108 | 104. 印尼爪哇语: `jw` 109 | 105. 英语: `en` 110 | 106. 约鲁巴语: `yo` 111 | 107. 越南语: `vi` 112 | 108. 中文(繁体): `zh-TW` 113 | 109. 中文(简体): `zh-CN` 114 | -------------------------------------------------------------------------------- /translator/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tx7do/go-utils/translator 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | cloud.google.com/go/translate v1.12.5 9 | github.com/stretchr/testify v1.10.0 10 | golang.org/x/text v0.25.0 11 | google.golang.org/api v0.233.0 12 | ) 13 | 14 | require ( 15 | cloud.google.com/go v0.121.1 // indirect 16 | cloud.google.com/go/auth v0.16.1 // indirect 17 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 18 | cloud.google.com/go/compute/metadata v0.7.0 // indirect 19 | cloud.google.com/go/longrunning v0.6.7 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/felixge/httpsnoop v1.0.4 // indirect 22 | github.com/go-logr/logr v1.4.2 // indirect 23 | github.com/go-logr/stdr v1.2.2 // indirect 24 | github.com/google/s2a-go v0.1.9 // indirect 25 | github.com/google/uuid v1.6.0 // indirect 26 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 27 | github.com/googleapis/gax-go/v2 v2.14.2 // indirect 28 | github.com/pmezard/go-difflib v1.0.0 // indirect 29 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 30 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 31 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 32 | go.opentelemetry.io/otel v1.36.0 // indirect 33 | go.opentelemetry.io/otel/metric v1.36.0 // indirect 34 | go.opentelemetry.io/otel/trace v1.36.0 // indirect 35 | golang.org/x/crypto v0.38.0 // indirect 36 | golang.org/x/net v0.40.0 // indirect 37 | golang.org/x/oauth2 v0.30.0 // indirect 38 | golang.org/x/sync v0.14.0 // indirect 39 | golang.org/x/sys v0.33.0 // indirect 40 | golang.org/x/time v0.11.0 // indirect 41 | google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237 // indirect 42 | google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect 43 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect 44 | google.golang.org/grpc v1.72.1 // indirect 45 | google.golang.org/protobuf v1.36.6 // indirect 46 | gopkg.in/yaml.v3 v3.0.1 // indirect 47 | ) 48 | 49 | replace github.com/tx7do/go-utils => ../ 50 | -------------------------------------------------------------------------------- /translator/google/options.go: -------------------------------------------------------------------------------- 1 | package google 2 | 3 | type Option func(*options) 4 | 5 | type options struct { 6 | version string 7 | apiKey string 8 | } 9 | 10 | func WithVersion(version string) Option { 11 | return func(o *options) { 12 | o.version = version 13 | } 14 | } 15 | 16 | func WithApiKey(key string) Option { 17 | return func(o *options) { 18 | o.apiKey = key 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /translator/google/translator.go: -------------------------------------------------------------------------------- 1 | package google 2 | 3 | import ( 4 | translateV2 "cloud.google.com/go/translate" 5 | translateV3 "cloud.google.com/go/translate/apiv3" 6 | ) 7 | 8 | type Translator struct { 9 | options *options 10 | 11 | clientV2 *translateV2.Client 12 | clientV3 *translateV3.TranslationClient 13 | } 14 | 15 | func NewTranslator(opts ...Option) *Translator { 16 | op := options{} 17 | for _, o := range opts { 18 | o(&op) 19 | } 20 | 21 | return &Translator{ 22 | options: &op, 23 | } 24 | } 25 | 26 | func (t *Translator) Translate(source, sourceLang, targetLang string) (string, error) { 27 | switch t.options.version { 28 | default: 29 | fallthrough 30 | case "v1": 31 | return t.TranslateV1(source, sourceLang, targetLang) 32 | 33 | case "v2": 34 | text, _, err := t.TranslateV2(source, sourceLang, targetLang) 35 | return text, err 36 | 37 | case "v3": 38 | return t.TranslateV3(source, sourceLang, targetLang) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /translator/google/translator_test.go: -------------------------------------------------------------------------------- 1 | package google 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "golang.org/x/text/language" 9 | ) 10 | 11 | func TestTranslateV1(t *testing.T) { 12 | translator := NewTranslator( 13 | WithVersion("v1"), 14 | ) 15 | assert.NotNil(t, translator) 16 | 17 | const text string = `Hello, World!` 18 | // you can use "auto" for source language 19 | // so, translator will detect language 20 | result, err := translator.TranslateV1(text, "en", "es") 21 | assert.Nil(t, err) 22 | fmt.Println(result) 23 | // Output: "Hola, Mundo!" 24 | 25 | result, err = translator.TranslateV1(text, "en", "zh-CN") 26 | assert.Nil(t, err) 27 | fmt.Println(result) 28 | 29 | result, err = translator.TranslateV1(text, "en", "lo") 30 | assert.Nil(t, err) 31 | fmt.Println(result) 32 | 33 | result, err = translator.TranslateV1(text, "en", "my") 34 | assert.Nil(t, err) 35 | fmt.Println(result) 36 | } 37 | 38 | func TestTranslateV2(t *testing.T) { 39 | translator := NewTranslator( 40 | WithVersion("v2"), 41 | WithApiKey("apikey"), 42 | ) 43 | assert.NotNil(t, translator) 44 | 45 | const text string = `Hello, World!` 46 | 47 | result, _, err := translator.TranslateV2(text, "en", "es") 48 | assert.Nil(t, err) 49 | fmt.Println(result) 50 | // Output: "Hola, Mundo!" 51 | 52 | result, _, err = translator.TranslateV2(text, "en", "zh-CN") 53 | assert.Nil(t, err) 54 | fmt.Println(result) 55 | 56 | result, _, err = translator.TranslateV2(text, "en", "lo") 57 | assert.Nil(t, err) 58 | fmt.Println(result) 59 | 60 | result, _, err = translator.TranslateV2(text, "en", "my") 61 | assert.Nil(t, err) 62 | fmt.Println(result) 63 | } 64 | 65 | func TestTranslateV3(t *testing.T) { 66 | translator := NewTranslator( 67 | WithVersion("v3"), 68 | WithApiKey("apikey"), 69 | ) 70 | assert.NotNil(t, translator) 71 | 72 | const text string = `Hello, World!` 73 | // you can use "auto" for source language 74 | // so, translator will detect language 75 | result, err := translator.TranslateV3(text, "en", "es") 76 | assert.Nil(t, err) 77 | fmt.Println(result) 78 | // Output: "Hola, Mundo!" 79 | 80 | result, err = translator.TranslateV3(text, "en", "zh-CN") 81 | assert.Nil(t, err) 82 | fmt.Println(result) 83 | 84 | result, err = translator.TranslateV3(text, "en", "lo") 85 | assert.Nil(t, err) 86 | fmt.Println(result) 87 | 88 | result, err = translator.TranslateV3(text, "en", "my") 89 | assert.Nil(t, err) 90 | fmt.Println(result) 91 | } 92 | 93 | func TestLanguageParse(t *testing.T) { 94 | lang, _ := language.Parse("en") 95 | assert.Equal(t, lang, language.English) 96 | fmt.Println(lang) 97 | fmt.Println(language.English.String()) 98 | } 99 | -------------------------------------------------------------------------------- /translator/google/utils.go: -------------------------------------------------------------------------------- 1 | package google 2 | 3 | import "net/url" 4 | 5 | // javascript "encodeURI()" 6 | // so we embed js to our golang program 7 | func encodeURI(s string) string { 8 | return url.QueryEscape(s) 9 | } 10 | -------------------------------------------------------------------------------- /translator/google/v1.go: -------------------------------------------------------------------------------- 1 | package google 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | func (t *Translator) TranslateV1(source, sourceLang, targetLang string) (string, error) { 13 | var text []string 14 | var result []interface{} 15 | 16 | encodedSource := encodeURI(source) 17 | uri := "https://translate.googleapis.com/translate_a/single?client=gtx&sl=" + 18 | sourceLang + "&tl=" + targetLang + "&dt=t&q=" + encodedSource 19 | 20 | r, err := http.Get(uri) 21 | if err != nil { 22 | return "err", errors.New("error getting translate.googleapis.com") 23 | } 24 | defer r.Body.Close() 25 | 26 | body, err := io.ReadAll(r.Body) 27 | if err != nil { 28 | return "err", errors.New("error reading response body") 29 | } 30 | 31 | bReq := strings.Contains(string(body), `Error 400 (Bad Request)`) 32 | if bReq { 33 | return "err", errors.New("error 400 (Bad Request)") 34 | } 35 | 36 | err = json.Unmarshal(body, &result) 37 | if err != nil { 38 | return "err", errors.New("error unmarshalling data") 39 | } 40 | 41 | if len(result) > 0 { 42 | inner := result[0] 43 | for _, slice := range inner.([]interface{}) { 44 | for _, translatedText := range slice.([]interface{}) { 45 | text = append(text, fmt.Sprintf("%v", translatedText)) 46 | break 47 | } 48 | } 49 | cText := strings.Join(text, "") 50 | 51 | return cText, nil 52 | } else { 53 | return "err", errors.New("no translated data in response") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /translator/google/v2.go: -------------------------------------------------------------------------------- 1 | package google 2 | 3 | import ( 4 | "context" 5 | 6 | translateV2 "cloud.google.com/go/translate" 7 | "golang.org/x/text/language" 8 | "google.golang.org/api/option" 9 | ) 10 | 11 | func (t *Translator) TranslateV2(source, _, targetLanguage string) (string, language.Tag, error) { 12 | ctx := context.Background() 13 | 14 | lang, err := language.Parse(targetLanguage) 15 | if err != nil { 16 | return "", language.English, err 17 | } 18 | 19 | if t.clientV2 == nil { 20 | client, err := translateV2.NewClient(ctx, option.WithAPIKey(t.options.apiKey)) 21 | if err != nil { 22 | return "", language.English, err 23 | } 24 | t.clientV2 = client 25 | } 26 | 27 | resp, err := t.clientV2.Translate(ctx, []string{source}, lang, nil) 28 | if err != nil { 29 | return "", language.English, err 30 | } 31 | 32 | return resp[0].Text, resp[0].Source, nil 33 | } 34 | -------------------------------------------------------------------------------- /translator/google/v3.go: -------------------------------------------------------------------------------- 1 | package google 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | translateV3 "cloud.google.com/go/translate/apiv3" 8 | "cloud.google.com/go/translate/apiv3/translatepb" 9 | "google.golang.org/api/option" 10 | ) 11 | 12 | func (t *Translator) TranslateV3(source, sourceLang, targetLang string) (string, error) { 13 | ctx := context.Background() 14 | 15 | if t.clientV3 == nil { 16 | client, err := translateV3.NewTranslationClient(ctx, option.WithAPIKey(t.options.apiKey)) 17 | if err != nil { 18 | return "", fmt.Errorf("NewTranslationClient: %w", err) 19 | } 20 | t.clientV3 = client 21 | } 22 | 23 | req := &translatepb.TranslateTextRequest{ 24 | SourceLanguageCode: sourceLang, 25 | TargetLanguageCode: targetLang, 26 | MimeType: "text/plain", // Mime types: "text/plain", "text/html" 27 | Contents: []string{source}, 28 | } 29 | 30 | resp, err := t.clientV3.TranslateText(ctx, req) 31 | if err != nil { 32 | return "", fmt.Errorf("TranslateText: %w", err) 33 | } 34 | 35 | if len(resp.GetTranslations()) != 1 { 36 | return "", fmt.Errorf("TranslateText: %w", err) 37 | } 38 | 39 | return resp.GetTranslations()[0].GetTranslatedText(), nil 40 | } 41 | -------------------------------------------------------------------------------- /translator/translator.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | // Translator 翻译器 4 | type Translator interface { 5 | // Translate 翻译 6 | Translate(source, sourceLang, targetLang string) (string, error) 7 | } 8 | -------------------------------------------------------------------------------- /translator/translator_test.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTranslator(t *testing.T) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /upgrade.bat: -------------------------------------------------------------------------------- 1 | echo off 2 | 3 | ::指定起始文件夹 4 | set DIR="%cd%" 5 | 6 | go get all 7 | go mod tidy 8 | 9 | cd %DIR%/bank_card 10 | go get all 11 | go mod tidy 12 | 13 | cd %DIR%/geoip 14 | go get all 15 | go mod tidy 16 | 17 | cd %DIR%/copierutil 18 | go get all 19 | go mod tidy 20 | 21 | cd %DIR%/translator 22 | go get all 23 | go mod tidy 24 | 25 | cd %DIR%/entgo 26 | go get all 27 | go mod tidy 28 | 29 | cd %DIR%/gorm 30 | go get all 31 | go mod tidy --------------------------------------------------------------------------------