├── .github └── workflows │ └── build.yml ├── README.md ├── app.go ├── build └── appicon.png ├── config └── init.go ├── frontend ├── auto-imports.d.ts ├── components.d.ts ├── index.html ├── package.json ├── package.json.md5 ├── src │ ├── App.vue │ ├── assets │ │ └── fonts │ │ │ └── ChillRoundM.otf │ ├── components │ │ ├── CaptureDialog.vue │ │ ├── PoolCard.vue │ │ └── SettingDialog.vue │ ├── main.ts │ ├── stores │ │ └── layout.ts │ ├── style.css │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── wailsjs │ ├── go │ ├── main │ │ ├── App.d.ts │ │ └── App.js │ └── models.ts │ └── runtime │ ├── package.json │ ├── runtime.d.ts │ └── runtime.js ├── go.mod ├── go.sum ├── logger └── init.go ├── logic ├── AppendLog.go ├── ExportRecord.go ├── FetchRemoteData.go ├── GetCommunityExchangeList.go ├── GetLatestVersion.go ├── GetLocalRecord.go ├── GetPoolInfo.go ├── GetUserInfoFromBBS.go ├── GetUserList.go ├── HandleCommunityTasks.go ├── ImportRecord.go ├── MergeEreRecord.go ├── MergeRecord.go ├── ParseEreExcelData.go ├── ParseEreJsonData.go ├── RemoveLocalRecord.go ├── SaveLocalRecord.go ├── UpdatePoolInfo.go └── UpdateTo.go ├── main.go ├── model ├── community.go ├── init.go ├── logInfo.go ├── pool.go ├── record.go └── request.go ├── pb ├── GachaData.pb.go ├── GachaTypeListData.pb.go ├── ItemData.pb.go ├── LangPackageTableCnData.pb.go └── LanguageStringData.pb.go ├── preload └── init.go ├── request ├── CommunityCommon.go ├── CommunityExchange.go ├── CommunityExchangeList.go ├── CommunityLogin.go ├── CommunitySign.go ├── CommunityTaskList.go ├── CommunityTopicLike.go ├── CommunityTopicList.go ├── CommunityTopicShare.go ├── CommunityTopicView.go └── CommunityUserInfo.go ├── sample.png ├── util ├── BackupDB.go ├── Cert.go ├── GetGameDataDir.go ├── GetLogInfo.go ├── GetTableData.go ├── SysProxy.go └── Version.go └── wails.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: release by tag 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | permissions: 7 | contents: write 8 | jobs: 9 | release-gf2gacha: 10 | runs-on: windows-latest 11 | steps: 12 | - name: checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Write Version 18 | run: | 19 | (Get-Content "util/Version.go") -replace 'const version = ""', 'const version = "${{ github.ref_name }}"' | Set-Content "util/Version.go" 20 | (Get-Content "wails.json") -replace '"productVersion": ""', '"productVersion": "${{ github.ref_name }}"' | Set-Content "wails.json" 21 | 22 | - name: Setup GoLang 23 | uses: actions/setup-go@v5 24 | with: 25 | check-latest: true 26 | go-version: 1.23 27 | 28 | - name: Setup NodeJS 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: '20.x' 32 | 33 | - name: Install Wails 34 | run: go install github.com/wailsapp/wails/v2/cmd/wails@latest 35 | 36 | - name: Build App 37 | working-directory: . 38 | run: wails build -webview2 embed -skipbindings -o gf2gacha.exe 39 | 40 | - name: Create folder and copy files 41 | run: | 42 | mkdir gf2gacha 43 | xcopy .\build\bin\gf2gacha.exe .\gf2gacha\ /y 44 | xcopy .\build\bin\gf2gacha.exe . /y 45 | 46 | - name: Zip the folder 47 | run: powershell Compress-Archive -Path gf2gacha -DestinationPath gf2gacha.zip 48 | 49 | - name: Upload to R2 50 | uses: ryand56/r2-upload-action@latest 51 | with: 52 | r2-account-id: ${{ secrets.R2_ACCOUNT_ID }} 53 | r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} 54 | r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} 55 | r2-bucket: ${{ secrets.R2_BUCKET }} 56 | source-dir: | 57 | build/bin/gf2gacha.exe 58 | gf2gacha.zip 59 | destination-dir: gf2gacha/${{ github.ref_name }}/ 60 | 61 | - name: Upload Release Asset 62 | uses: softprops/action-gh-release@v2 63 | with: 64 | files: | 65 | gf2gacha.zip 66 | gf2gacha.exe 67 | 68 | 69 | - name: Build Changelog 70 | run: npx changelogithub 71 | env: 72 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 少女前线2:追放 抽卡导出分析工具 2 | 3 | ## 为什么做这个工具? 4 | 5 | [追放采购记录导出工具](https://github.com/EtherealAO/exilium-recruit-export)作者在其[NGA发布贴](https://bbs.nga.cn/read.php?tid=38812531)中表示不搞了,希望有人接盘 6 | 7 | 但我并不熟悉`Electron`,也不太喜欢`Electron`直接包一个巨大浏览器的做法 8 | 9 | 于是把之前自用的导出程序用更加轻量的`Wails`包了个界面传了上来 10 | 11 | ![image](sample.png) 12 | 13 | ## 主要功能 14 | 15 | - [x] 增量更新 `拉取服务端最新的抽卡数据,与本地数据合并` 16 | - [x] 全量更新 `拉取服务端所有抽卡数据,与本地数据合并(目前合并算法准备重写,完成后再做)` 17 | - [x] 一键社区 `一键完成社区签到与任务,并根据自定义设置兑换道具` 18 | - [x] 导入Ere数据 `导入EreJson或EreExcel格式的数据,与本地数据合并(合并前会自动备份数据库)` 19 | - [x] 导入/导出RawJson `导入/导出服务器原始格式的数据的map` 20 | - [x] 导入/导出MccExcel `导入/导出Mcc格式的Excel` 21 | 22 | ## 基本原理 23 | 24 | 本软件通过读取日志获取`游戏路径`、`抽卡链接`、`UID`、`AccessToken`,进而从服务器获取抽卡记录 25 | 26 | 本软件会在当前目录下的生成名为`gf2gacha.db`的`sqlite`数据库,这就是你的抽卡数据了,你可以使用任何支持sqlite的数据库管理工具查看其内容 27 | 28 | ## 特别提醒 29 | 30 | 本软件不会将用户数据传输到任何第三方服务上,数据仅存储在你本地 31 | 32 | 进行高危操作时(如合并数据),数据库会自动备份 33 | 34 | 如果你有云端托管数据的需求,请点击软件右上角的`回形针图标`查看你当前日志信息,将其复制给第三方服务提供方 35 | 36 | 注意!`AccessToken`是你的临时登录凭证,请勿随意泄露 37 | 38 | ## FAQ 39 | 40 | ### 为什么看不到新手池数据?为什么角色池/武器池/常驻池缺失数据? 41 | 42 | 追放服务器只保留最近180天的数据,大多数抽卡游戏都有类似的限制,所以有需求的用户才会采取多种办法来保存抽卡记录 43 | 44 | ### 如何导入来自[追放采购记录导出工具](https://github.com/EtherealAO/exilium-recruit-export)的数据? 45 | 46 | 由于ERE的UID使用的UID并不是游戏内的UID,我无法判断原数据是属于哪个用户 47 | 48 | **请务必先在主界面手动选择你要导入旧数据的用户对应的UID** 49 | 50 | 然后点击`导入ERE数据`按钮,在ERE程序目录中找到`userData`文件夹,选择里面类似`gacha-list-1234567890.json`的文件,点击确定即可 51 | 52 | ## 附录 53 | 54 | ### 卡池类型 55 | 56 | | PoolType | 备注 | 57 | |----------|-------| 58 | | 1 | 常驻池 | 59 | | 2 | 暂无 | 60 | | 3 | 角色池 | 61 | | 4 | 武器池 | 62 | | 5 | 新手池 | 63 | | 6 | 自选角色池 | 64 | | 7 | 自选武器池 | 65 | | 8 | 神秘箱 | 66 | 67 | ### 卡池信息 68 | 69 | | PoolId | 备注 | 时间戳起 | 时间戳止 | 时间起 | 时间止 | 70 | |--------|--------------|------------|------------|-------------------------------|-------------------------------| 71 | | 1001 | 常规采购 | | | | | 72 | | 2001 | 军备提升β-托洛洛武器 | 1703124000 | 1704358799 | 2023-12-21 10:00:00 +0800 CST | 2024-01-04 16:59:59 +0800 CST | 73 | | 3001 | 定向采购β-托洛洛 | 1703124000 | 1704358799 | 2023-12-21 10:00:00 +0800 CST | 2024-01-04 16:59:59 +0800 CST | 74 | | 4001 | 初始采购-新手池 | | | | | 75 | | 5001 | 军备提升α-黛烟武器 | 1703494800 | 1705913999 | 2023-12-25 17:00:00 +0800 CST | 2024-01-22 16:59:59 +0800 CST | 76 | | 6001 | 定向采购α-黛烟 | 1703494800 | 1705913999 | 2023-12-25 17:00:00 +0800 CST | 2024-01-22 16:59:59 +0800 CST | 77 | | 7001 | 定向采购β-塞布丽娜 | 1705395600 | 1706605199 | 2024-01-16 17:00:00 +0800 CST | 2024-01-30 16:59:59 +0800 CST | 78 | | 8001 | 军备提升β-塞布丽娜武器 | 1705395600 | 1706605199 | 2024-01-16 17:00:00 +0800 CST | 2024-01-30 16:59:59 +0800 CST | 79 | | 9001 | 定向采购α-桑朵莱希 | 1705978800 | 1708657199 | 2024-01-23 11:00:00 +0800 CST | 2024-02-23 10:59:59 +0800 CST | 80 | | 10001 | 军备提升α-桑朵莱希武器 | 1705978800 | 1708657199 | 2024-01-23 11:00:00 +0800 CST | 2024-02-23 10:59:59 +0800 CST | 81 | | 11001 | 定向采购β-琼玖 | 1707382800 | 1708657199 | 2024-02-08 17:00:00 +0800 CST | 2024-02-23 10:59:59 +0800 CST | 82 | | 12001 | 军备提升β-琼玖武器 | 1707382800 | 1708657199 | 2024-02-08 17:00:00 +0800 CST | 2024-02-23 10:59:59 +0800 CST | 83 | | 13001 | 定向采购α-莱娜 | 1708657200 | 1711421999 | 2024-02-23 11:00:00 +0800 CST | 2024-03-26 10:59:59 +0800 CST | 84 | | 14001 | 军备提升α-莱娜武器 | 1708657200 | 1711421999 | 2024-02-23 11:00:00 +0800 CST | 2024-03-26 10:59:59 +0800 CST | 85 | | 15001 | 定向采购-黛烟 | 1712653200 | 1714445999 | 2024-04-09 17:00:00 +0800 CST | 2024-04-30 10:59:59 +0800 CST | 86 | | 16001 | 军备提升-黛烟武器 | 1712653200 | 1714445999 | 2024-04-09 17:00:00 +0800 CST | 2024-04-30 10:59:59 +0800 CST | 87 | | 17001 | 定向采购-绛雨 | 1711422000 | 1714445999 | 2024-03-26 11:00:00 +0800 CST | 2024-04-30 10:59:59 +0800 CST | 88 | | 18001 | 军备提升-绛雨武器 | 1711422000 | 1714445999 | 2024-03-26 11:00:00 +0800 CST | 2024-04-30 10:59:59 +0800 CST | 89 | | 19001 | 定向采购β-莫辛纳甘 | 1709629200 | 1711421999 | 2024-03-05 17:00:00 +0800 CST | 2024-03-26 10:59:59 +0800 CST | 90 | | 20001 | 军备提升β-莫辛纳甘武器 | 1709629200 | 1711421999 | 2024-03-05 17:00:00 +0800 CST | 2024-03-26 10:59:59 +0800 CST | 91 | | 21001 | 定向采购-佩里缇亚 | 1711422000 | 1712653199 | 2024-03-26 11:00:00 +0800 CST | 2024-04-09 16:59:59 +0800 CST | 92 | | 22001 | 军备提升-佩里缇亚武器 | 1711422000 | 1712653199 | 2024-03-26 11:00:00 +0800 CST | 2024-04-09 16:59:59 +0800 CST | 93 | | 23001 | 定向采购-玛绮朵 | 1714446000 | 1717556399 | 2024-04-30 11:00:00 +0800 CST | 2024-06-05 10:59:59 +0800 CST | 94 | | 24001 | 军备提升-玛绮朵武器 | 1714446000 | 1717556399 | 2024-04-30 11:00:00 +0800 CST | 2024-06-05 10:59:59 +0800 CST | 95 | | 25001 | 定向采购-桑朵莱希 | 1715677200 | 1717556399 | 2024-05-14 17:00:00 +0800 CST | 2024-06-05 10:59:59 +0800 CST | 96 | | 26001 | 军备提升-桑朵莱希武器 | 1715677200 | 1717556399 | 2024-05-14 17:00:00 +0800 CST | 2024-06-05 10:59:59 +0800 CST | 97 | | 27001 | 定向采购-乌尔丽德 | 1717556400 | 1719284399 | 2024-06-05 11:00:00 +0800 CST | 2024-06-25 10:59:59 +0800 CST | 98 | | 28001 | 军备提升-乌尔丽德武器 | 1717556400 | 1719284399 | 2024-06-05 11:00:00 +0800 CST | 2024-06-25 10:59:59 +0800 CST | 99 | | 29001 | 定向采购-莱娜 | 1717556400 | 1719284399 | 2024-06-05 11:00:00 +0800 CST | 2024-06-25 10:59:59 +0800 CST | 100 | | 30001 | 军备提升-莱娜武器 | 1717556400 | 1719284399 | 2024-06-05 11:00:00 +0800 CST | 2024-06-25 10:59:59 +0800 CST | 101 | | 31001 | 定向采购-索米 | 1719284400 | 1721098799 | 2024-06-25 11:00:00 +0800 CST | 2024-07-16 10:59:59 +0800 CST | 102 | | 32001 | 军备提升-索米武器 | 1719284400 | 1721098799 | 2024-06-25 11:00:00 +0800 CST | 2024-07-16 10:59:59 +0800 CST | 103 | | 33001 | 定向采购-绛雨 | 1719284400 | 1721098799 | 2024-06-25 11:00:00 +0800 CST | 2024-07-16 10:59:59 +0800 CST | 104 | | 34001 | 军备提升-绛雨武器 | 1719284400 | 1721098799 | 2024-06-25 11:00:00 +0800 CST | 2024-07-16 10:59:59 +0800 CST | 105 | | 35001 | 定向采购-杜莎妮 | 1721098800 | 1722913199 | 2024-07-16 11:00:00 +0800 CST | 2024-08-06 10:59:59 +0800 CST | 106 | | 36001 | 军备提升-杜莎妮武器 | 1721098800 | 1722913199 | 2024-07-16 11:00:00 +0800 CST | 2024-08-06 10:59:59 +0800 CST | 107 | | 37001 | 定向采购-玛绮朵 | 1721098800 | 1722913199 | 2024-07-16 11:00:00 +0800 CST | 2024-08-06 10:59:59 +0800 CST | 108 | | 38001 | 军备提升-玛绮朵武器 | 1721098800 | 1722913199 | 2024-07-16 11:00:00 +0800 CST | 2024-08-06 10:59:59 +0800 CST | 109 | | 39001 | 定向采购-朝晖 | 1722913200 | 1724705999 | 2024-08-06 11:00:00 +0800 CST | 2024-08-27 04:59:59 +0800 CST | 110 | | 40001 | 军备提升-朝晖武器 | 1722913200 | 1724705999 | 2024-08-06 11:00:00 +0800 CST | 2024-08-27 04:59:59 +0800 CST | 111 | | 41001 | 定向采购-乌尔丽德 | 1722913200 | 1724705999 | 2024-08-06 11:00:00 +0800 CST | 2024-08-27 04:59:59 +0800 CST | 112 | | 42001 | 军备提升-乌尔丽德武器 | 1722913200 | 1724705999 | 2024-08-06 11:00:00 +0800 CST | 2024-08-27 04:59:59 +0800 CST | 113 | | 43001 | 定向采购-可露凯 | 1724706000 | 1726714799 | 2024-08-27 05:00:00 +0800 CST | 2024-09-19 10:59:59 +0800 CST | 114 | | 44001 | 军备提升-可露凯武器 | 1724706000 | 1726714799 | 2024-08-27 05:00:00 +0800 CST | 2024-09-19 10:59:59 +0800 CST | 115 | | 45001 | 定向采购-索米 | 1724706000 | 1726714799 | 2024-08-27 05:00:00 +0800 CST | 2024-09-19 10:59:59 +0800 CST | 116 | | 46001 | 军备提升-索米武器 | 1724706000 | 1726714799 | 2024-08-27 05:00:00 +0800 CST | 2024-09-19 10:59:59 +0800 CST | 117 | | 90001 | 自选采购·人形 | 1724706000 | 1728442799 | 2024-08-27 05:00:00 +0800 CST | 2024-10-09 10:59:59 +0800 CST | 118 | | 91001 | 自选采购·军备 | 1724706000 | 1728442799 | 2024-08-27 05:00:00 +0800 CST | 2024-10-09 10:59:59 +0800 CST | 119 | | 99001 | 神秘箱 | 1689476400 | 4089668399 | 2023-07-16 11:00:00 +0800 CST | 2099-08-06 10:59:59 +0800 CST | 120 | 121 | ### 人形信息 122 | 123 | | ItemId | Rank | 备注 | 124 | |---------------------------------|------------------------------|---------------------------------| 125 | | 1001 | 4 | 克罗丽科 | 126 | | 1008 | 4 | 纳美西丝 | 127 | | 1009 | 4 | 寇尔芙 | 128 | | 1013 | 5 | 莱娜 | 129 | | 1015 | 5 | 维普蕾 | 130 | | 1017 | 4 | 闪电 | 131 | | 1021 | 5 | 佩里缇亚 | 132 | | 1022 | 4 | 夏克里 | 133 | | 1023 | 5 | 杜莎妮 | 134 | | 1024 | 4 | 奇塔 | 135 | | 1025 | 5 | 托洛洛 | 136 | | 1026 | 4 | 纳甘 | 137 | | 1027 | 5 | 琼玖 | 138 | | 1028 | 5 | 桑朵莱希 | 139 | | 1029 | 5 | 塞布丽娜 | 140 | | 1032 | 5 | 黛烟 | 141 | | 1033 | 5 | 莫辛纳甘 | 142 | | 1034 | 5 | 玛绮朵 | 143 | | 1035 | 5 | 绛雨 | 144 | | 1036 | 4 | 科谢尼娅 | 145 | | 1037 | 5 | 乌尔丽德 | 146 | | 1038 | 4 | 莉塔拉 | 147 | | 1039 | 5 | 索米 | 148 | | 1040 | 5 | 波波沙 | 149 | | 1041 | 4 | 洛塔 | 150 | | 1050 | 5 | 朝晖 | 151 | | 1052 | 5 | 可露凯 | 152 | 153 | ### 武器信息 154 | 155 | | ItemId | Rank | 备注 | 156 | |----------------------------------|------------------------------|-----------------------------------------| 157 | | 10001 | 5 | 喧闹恶灵 | 158 | | 10002 | 5 | 绝密手稿 | 159 | | 10003 | 5 | 阿尔克纳 | 160 | | 10004 | 5 | 盖尔诺 | 161 | | 10005 | 5 | 王冠鹿角兔 | 162 | | 10006 | 5 | 妙尔尼尔 | 163 | | 10007 | 5 | 远行游鸽 | 164 | | 10131 | 3 | 旧式通用冲锋枪9 | 165 | | 10132 | 4 | 通用冲锋枪9 | 166 | | 10133 | 5 | 幼狮 | 167 | | 10231 | 3 | 旧式科夫罗夫 | 168 | | 10232 | 4 | 科夫罗夫 | 169 | | 10233 | 5 | 传颂之诗 | 170 | | 10331 | 3 | 旧式莫辛-纳甘 | 171 | | 10332 | 4 | 莫辛-纳甘 | 172 | | 10333 | 5 | 斯摩希克 | 173 | | 10341 | 3 | 旧式瓦尔特2000 | 174 | | 10342 | 4 | 瓦尔特2000 | 175 | | 10343 | 5 | 苦涩焦糖 | 176 | | 10351 | 3 | 旧式九七式 | 177 | | 10352 | 4 | 九七式 | 178 | | 10353 | 5 | 跃虎 | 179 | | 10361 | 3 | 旧式斯捷奇金 | 180 | | 10362 | 4 | 斯捷奇金 | 181 | | 10371 | 3 | 旧式羽锋 | 182 | | 10372 | 4 | 羽锋 | 183 | | 10373 | 5 | 流羽白英 | 184 | | 10381 | 3 | 旧式加利尔轻机枪 | 185 | | 10382 | 4 | 加利尔轻机枪 | 186 | | 10391 | 3 | 旧式索米 | 187 | | 10392 | 4 | 索米 | 188 | | 10393 | 5 | 未言使命 | 189 | | 10401 | 3 | 旧式波波沙冲锋枪 | 190 | | 10402 | 4 | 波波沙冲锋枪 | 191 | | 10403 | 5 | 斯瓦罗格 | 192 | | 10411 | 3 | 旧式超级90 | 193 | | 10412 | 4 | 超级90 | 194 | | 10501 | 3 | 旧式长风零六 | 195 | | 10502 | 4 | 长风零六 | 196 | | 10503 | 5 | 不留行 | 197 | | 10521 | 3 | 旧式黑克勒科赫416 | 198 | | 10522 | 4 | 黑克勒科赫416 | 199 | | 10523 | 5 | 斯库拉 | 200 | | 11007 | 4 | 野兔 | 201 | | 11008 | 3 | 旧式金牛座曲线 | 202 | | 11009 | 3 | 旧式复仇女神 | 203 | | 11010 | 3 | 旧式格洛利娅 | 204 | | 11014 | 4 | 复仇女神 | 205 | | 11015 | 4 | 金牛座曲线 | 206 | | 11016 | 5 | 猎心者 | 207 | | 11017 | 3 | 旧式莫洛12 | 208 | | 11020 | 5 | 光学幻境 | 209 | | 11021 | 4 | 莫洛12 | 210 | | 11022 | 3 | 旧式野兔 | 211 | | 11023 | 4 | 格洛利娅 | 212 | | 11024 | 3 | 旧式佩切涅 | 213 | | 11026 | 4 | 佩切涅 | 214 | | 11030 | 3 | 旧式罗宾逊先进步枪 | 215 | | 11031 | 4 | 罗宾逊先进步枪 | 216 | | 11036 | 3 | 旧式卡拉什-阿尔法 | 217 | | 11037 | 4 | 卡拉什-阿尔法 | 218 | | 11038 | 5 | 游星 | 219 | | 11039 | 3 | 旧式黑科赫7 | 220 | | 11040 | 4 | 黑科赫7 | 221 | | 11042 | 3 | 旧式一九一式 | 222 | | 11043 | 4 | 一九一式 | 223 | | 11044 | 5 | 金石奏 | 224 | | 11045 | 3 | 旧式特殊用途自动型霰弹枪 | 225 | | 11046 | 4 | 特殊用途自动型霰弹枪 | 226 | | 11047 | 5 | 梅扎露娜 | 227 | | 11048 | 3 | 旧式纳甘左轮 | 228 | | 11049 | 4 | 纳甘左轮 | 229 | | 11051 | 3 | 旧式九五式 | 230 | | 11052 | 4 | 九五式 | 231 | | 11053 | 5 | 重弦 | 232 | | 11054 | 3 | 旧式格威尔36 | 233 | | 11055 | 4 | 格威尔36 | 234 | | 11056 | 5 | 女仆准则 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "gf2gacha/config" 7 | "gf2gacha/logger" 8 | "gf2gacha/logic" 9 | "gf2gacha/model" 10 | "gf2gacha/util" 11 | "github.com/elazarl/goproxy" 12 | "github.com/pkg/errors" 13 | "github.com/wailsapp/wails/v2/pkg/runtime" 14 | "net" 15 | "net/http" 16 | "os" 17 | "strings" 18 | "sync" 19 | ) 20 | 21 | // App struct 22 | type App struct { 23 | ctx context.Context 24 | captureMutex sync.Mutex 25 | tcpPort int 26 | tcpListener *net.Listener 27 | MitmServer *http.Server 28 | } 29 | 30 | // NewApp creates a new App application struct 31 | func NewApp() *App { 32 | return &App{} 33 | } 34 | 35 | // startup is called when the app starts. The context is saved 36 | // so we can call the runtime methods 37 | func (a *App) startup(ctx context.Context) { 38 | a.ctx = ctx 39 | } 40 | 41 | func (a *App) GetLogInfo() model.LogInfo { 42 | info, err := util.GetLogInfo() 43 | if err != nil { 44 | logger.Logger.Error(err) 45 | return model.LogInfo{} 46 | } 47 | return info 48 | } 49 | 50 | func (a *App) GetUserList() []string { 51 | userList, err := logic.GetUserList() 52 | if err != nil { 53 | logger.Logger.Error(err) 54 | return nil 55 | } 56 | return userList 57 | } 58 | 59 | func (a *App) GetPoolInfo(uid string, poolType int64) model.Pool { 60 | pool, err := logic.GetPoolInfo(uid, poolType) 61 | if err != nil { 62 | logger.Logger.Error(err) 63 | return model.Pool{} 64 | } 65 | return pool 66 | } 67 | 68 | func (a *App) UpdatePoolInfo(isFull bool) ([]string, error) { 69 | messageList, err := logic.UpdatePoolInfo(isFull) 70 | if err != nil { 71 | logger.Logger.Error(err) 72 | return nil, err 73 | } 74 | return messageList, nil 75 | } 76 | 77 | func (a *App) MergeEreRecord(uid, typ string) (message string, err error) { 78 | if uid == "" { 79 | return "", errors.New("UID为空,请至少更新一次数据再进行合并") 80 | } 81 | 82 | var fileOption runtime.OpenDialogOptions 83 | switch strings.ToLower(typ) { 84 | case "json": 85 | fileOption.Title = "请选择Exilium Recruit Export的Json文件" 86 | fileOption.Filters = []runtime.FileFilter{{DisplayName: "EreJsonData", Pattern: "*.json"}} 87 | case "excel": 88 | fileOption.Title = "请选择Exilium Recruit Export的Excel文件" 89 | fileOption.Filters = []runtime.FileFilter{{DisplayName: "EreExcelData", Pattern: "*.xlsx"}} 90 | default: 91 | return "", errors.Errorf("unknown type: %s", typ) 92 | } 93 | erePath, err := runtime.OpenFileDialog(a.ctx, fileOption) 94 | if err != nil { 95 | logger.Logger.Error(err) 96 | return 97 | } 98 | 99 | if erePath == "" { 100 | return "", errors.New("用户取消导入") 101 | } 102 | 103 | err = logic.MergeEreRecord(uid, erePath, typ) 104 | if err != nil { 105 | logger.Logger.Error(err) 106 | return 107 | } 108 | 109 | message = "合并成功" 110 | return 111 | } 112 | 113 | func (a *App) ImportRawJson(uid string, isReverse bool) (message string, err error) { 114 | if uid == "" { 115 | return "", errors.New("UID为空,请至少更新一次数据再进行导出") 116 | } 117 | 118 | fileOption := runtime.OpenDialogOptions{ 119 | Title: "请选择RawJson文件", 120 | Filters: []runtime.FileFilter{{DisplayName: "RawJsonData", Pattern: "*.json"}}, 121 | } 122 | importFilePath, err := runtime.OpenFileDialog(a.ctx, fileOption) 123 | if err != nil { 124 | logger.Logger.Error(err) 125 | return 126 | } 127 | 128 | if importFilePath == "" { 129 | return "", errors.New("用户取消导入") 130 | } 131 | 132 | err = logic.ImportRawJson(uid, importFilePath, isReverse) 133 | if err != nil { 134 | logger.Logger.Error(err) 135 | return 136 | } 137 | 138 | message = "合并成功" 139 | return 140 | } 141 | 142 | func (a *App) ExportRawJson(uid string) (message string, err error) { 143 | if uid == "" { 144 | return "", errors.New("UID为空,请至少更新一次数据再进行导出") 145 | } 146 | 147 | fileOption := runtime.OpenDialogOptions{ 148 | Title: "选择RawJson保存目录", 149 | } 150 | saveDir, err := runtime.OpenDirectoryDialog(a.ctx, fileOption) 151 | if err != nil { 152 | logger.Logger.Error(err) 153 | return 154 | } 155 | 156 | if saveDir == "" { 157 | return "", errors.New("用户取消导出") 158 | } 159 | 160 | err = logic.ExportRawJson(uid, saveDir) 161 | if err != nil { 162 | logger.Logger.Error(err) 163 | return 164 | } 165 | 166 | message = "导出成功" 167 | return 168 | } 169 | 170 | func (a *App) ImportMccExcel(uid string) (message string, err error) { 171 | if uid == "" { 172 | return "", errors.New("UID为空,请至少更新一次数据再进行导出") 173 | } 174 | 175 | fileOption := runtime.OpenDialogOptions{ 176 | Title: "请选择MccExcel文件", 177 | Filters: []runtime.FileFilter{{DisplayName: "MccExcel", Pattern: "*.xlsx"}}, 178 | } 179 | importFilePath, err := runtime.OpenFileDialog(a.ctx, fileOption) 180 | if err != nil { 181 | logger.Logger.Error(err) 182 | return 183 | } 184 | 185 | if importFilePath == "" { 186 | return "", errors.New("用户取消导入") 187 | } 188 | 189 | err = logic.ImportMccExcel(uid, importFilePath) 190 | if err != nil { 191 | logger.Logger.Error(err) 192 | return 193 | } 194 | 195 | message = "MccExcel合并成功" 196 | return 197 | } 198 | 199 | func (a *App) ExportMccExcel(uid string) (message string, err error) { 200 | if uid == "" { 201 | return "", errors.New("UID为空,请至少更新一次数据再进行导出") 202 | } 203 | 204 | fileOption := runtime.OpenDialogOptions{ 205 | Title: "选择MccExcel保存目录", 206 | } 207 | saveDir, err := runtime.OpenDirectoryDialog(a.ctx, fileOption) 208 | if err != nil { 209 | logger.Logger.Error(err) 210 | return 211 | } 212 | 213 | if saveDir == "" { 214 | return "", errors.New("用户取消导出") 215 | } 216 | 217 | err = logic.ExportMccExcel(uid, saveDir) 218 | if err != nil { 219 | logger.Logger.Error(err) 220 | return 221 | } 222 | 223 | message = "MccExcel导出成功" 224 | return 225 | } 226 | 227 | func (a *App) HandleCommunityTasks() (messageList []string, err error) { 228 | messageList, err = logic.HandleCommunityTasks() 229 | if err != nil { 230 | logger.Logger.Error(err) 231 | return 232 | } 233 | return 234 | } 235 | 236 | func (a *App) GetCurrentVersion() string { 237 | return util.GetVersion() 238 | } 239 | 240 | func (a *App) GetLatestVersion() (string, error) { 241 | version, err := logic.GetLatestVersion() 242 | if err != nil { 243 | logger.Logger.Error(err) 244 | return "", err 245 | } 246 | return version, nil 247 | } 248 | 249 | func (a *App) UpdateTo(version string) (string, error) { 250 | err := logic.UpdateTo(version) 251 | if err != nil { 252 | logger.Logger.Error(err) 253 | return "", err 254 | } 255 | return "", nil 256 | } 257 | 258 | func (a *App) GetCommunityExchangeList() ([]model.CommunityExchangeList, error) { 259 | list, err := logic.GetCommunityExchangeList() 260 | if err != nil { 261 | logger.Logger.Error(err) 262 | return nil, err 263 | } 264 | return list, nil 265 | } 266 | 267 | func (a *App) GetSettingExchangeList() ([]int64, error) { 268 | if !config.IsSetExchangeList() { 269 | exchangeList, err := logic.GetCommunityExchangeList() 270 | if err != nil { 271 | logger.Logger.Error(err) 272 | return nil, err 273 | } 274 | var idList []int64 275 | for _, item := range exchangeList { 276 | idList = append(idList, item.Id) 277 | } 278 | err = config.SetExchangeList(idList) 279 | if err != nil { 280 | logger.Logger.Error(err) 281 | return nil, err 282 | } 283 | } 284 | 285 | return config.GetExchangeList(), nil 286 | } 287 | 288 | func (a *App) SaveSettingExchangeList(exchangeList []int64) error { 289 | return config.SetExchangeList(exchangeList) 290 | } 291 | 292 | func (a *App) GetSettingFont() (string, error) { 293 | return config.GetFont(), nil 294 | } 295 | 296 | func (a *App) SaveSettingFont(newFont string) error { 297 | return config.SetFont(newFont) 298 | } 299 | 300 | func (a *App) GetSettingLayout() (int64, error) { 301 | return config.GetLayout(), nil 302 | } 303 | 304 | func (a *App) SaveSettingLayout(layoutType int64) error { 305 | return config.SetLayout(layoutType) 306 | } 307 | 308 | func (a *App) CaptureStart() error { 309 | _, err := os.Stat("ca.crt") 310 | if err != nil { 311 | logger.Logger.Errorf("CA证书不存在:%v", err) 312 | err = util.GenCA() 313 | if err != nil { 314 | logger.Logger.Errorf("生成CA证书出错:%v", err) 315 | return err 316 | } 317 | } 318 | 319 | if !util.IsTrustedCA() { 320 | err = util.InstallCA() 321 | if err != nil { 322 | logger.Logger.Errorf("安装CA证书出错:%v", err) 323 | return err 324 | } 325 | } 326 | 327 | if a.tcpListener == nil { 328 | for port := 60000; port < 60010; port++ { 329 | listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) 330 | if err != nil { 331 | logger.Logger.Error(err) 332 | continue 333 | } 334 | 335 | a.captureMutex.Lock() 336 | a.tcpPort = port 337 | a.tcpListener = &listener 338 | a.captureMutex.Unlock() 339 | logger.Logger.Infof("端口%d可用\n", port) 340 | 341 | break 342 | } 343 | } 344 | 345 | cert, err := util.ParseCA() 346 | if err != nil { 347 | logger.Logger.Errorf("读取CA证书出错:%v", err) 348 | return err 349 | } 350 | 351 | customCaMitm := &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(cert)} 352 | var customAlwaysMitm goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { 353 | return customCaMitm, host 354 | } 355 | 356 | proxy := goproxy.NewProxyHttpServer() 357 | cond := goproxy.DstHostIs("gf2-gacha-record.sunborngame.com") 358 | 359 | proxy.OnRequest(cond).HandleConnect(customAlwaysMitm) 360 | proxy.OnRequest(cond).DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 361 | if req.URL.Path == "/list" { 362 | gachaUrl := req.URL.String() 363 | accessToken := req.Header.Get("Authorization") 364 | 365 | userInfo, err := logic.GetUserInfoFromBBS(accessToken) 366 | if err != nil { 367 | logger.Logger.Errorf("未从社区获取到用户信息:%v", err) 368 | return req, nil 369 | } 370 | 371 | err = logic.AppendLog(accessToken, userInfo.User.GameUid, gachaUrl) 372 | if err != nil { 373 | logger.Logger.Errorf("追加游戏日志失败:%v", err) 374 | return req, nil 375 | } 376 | 377 | logger.Logger.Infof("uid: %d\n", userInfo.User.GameUid) 378 | logger.Logger.Infof("gachaUrl: %s\n", gachaUrl) 379 | logger.Logger.Infof("accessToken: %s\n", accessToken) 380 | 381 | runtime.EventsEmit(a.ctx, "captureSuccess") 382 | } 383 | 384 | return req, nil 385 | }) 386 | 387 | //启动监听 388 | if a.MitmServer == nil { 389 | a.captureMutex.Lock() 390 | a.MitmServer = &http.Server{Handler: proxy} 391 | 392 | go func(mitmServer *http.Server, tcpListener *net.Listener) { 393 | logger.Logger.Info("捕获协程启动") 394 | err := mitmServer.Serve(*tcpListener) 395 | if err != nil { 396 | if errors.Is(err, http.ErrServerClosed) { 397 | logger.Logger.Info("捕获正常停止") 398 | return 399 | } else { 400 | logger.Logger.Errorf("捕获异常停止: %v", err) 401 | } 402 | a.captureMutex.Lock() 403 | a.tcpListener = nil 404 | a.MitmServer = nil 405 | a.captureMutex.Unlock() 406 | } 407 | }(a.MitmServer, a.tcpListener) 408 | 409 | err = util.EnableSysProxy(a.tcpPort) 410 | if err != nil { 411 | logger.Logger.Errorf("设置代理信息出错:%v", err) 412 | return err 413 | } 414 | 415 | a.captureMutex.Unlock() 416 | } 417 | 418 | return nil 419 | } 420 | 421 | func (a *App) CaptureClose() error { 422 | a.captureMutex.Lock() 423 | defer a.captureMutex.Unlock() 424 | 425 | if a.MitmServer != nil { 426 | err := a.MitmServer.Shutdown(context.Background()) 427 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 428 | logger.Logger.Error(err) 429 | return err 430 | } 431 | a.tcpListener = nil 432 | a.MitmServer = nil 433 | } 434 | 435 | err := util.DisableSysProxy() 436 | if err != nil { 437 | logger.Logger.Errorf("还原代理信息出错:%v", err) 438 | return err 439 | } 440 | 441 | return nil 442 | } 443 | -------------------------------------------------------------------------------- /build/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatchaCabin/gf2gacha/98957835ff3b0b90afa5af33d84bb44ba4e4f34d/build/appicon.png -------------------------------------------------------------------------------- /config/init.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "gf2gacha/logger" 6 | "github.com/pkg/errors" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func init() { 11 | viper.SetConfigName("config") 12 | viper.SetConfigType("yaml") 13 | viper.AddConfigPath(".") 14 | if err := viper.ReadInConfig(); err != nil { 15 | if errors.As(err, &viper.ConfigFileNotFoundError{}) { 16 | logger.Logger.Warnf("配置文件不存在,将自动创建默认配置文件") 17 | err = viper.SafeWriteConfig() 18 | if err != nil { 19 | logger.Logger.Panic(err) 20 | } 21 | } else { 22 | logger.Logger.Panic(err) 23 | } 24 | } 25 | } 26 | 27 | func GetGameDataDir() string { 28 | return viper.GetString("GameDataDir") 29 | } 30 | 31 | func SetGameDataDir(gameDataDir string) error { 32 | viper.Set("GameDataDir", gameDataDir) 33 | return viper.WriteConfig() 34 | } 35 | 36 | func IsSetGameDataDir() bool { 37 | return viper.IsSet("GameDataDir") 38 | } 39 | 40 | func GetWebToken(uid string) string { 41 | return viper.GetString(fmt.Sprintf("%s.webToken", uid)) 42 | } 43 | 44 | func SetWebToken(uid, token string) error { 45 | viper.Set(fmt.Sprintf("%s.webToken", uid), token) 46 | return viper.WriteConfig() 47 | } 48 | 49 | func IsSetExchangeList() bool { 50 | return viper.IsSet("exchangeList") 51 | } 52 | 53 | func GetExchangeList() []int64 { 54 | intList := viper.GetIntSlice("exchangeList") 55 | var int64List []int64 56 | for i := range intList { 57 | int64List = append(int64List, int64(intList[i])) 58 | } 59 | 60 | return int64List 61 | } 62 | 63 | func SetExchangeList(exchangeList []int64) error { 64 | viper.Set("exchangeList", exchangeList) 65 | return viper.WriteConfig() 66 | } 67 | 68 | func GetFont() string { 69 | return viper.GetString("font") 70 | } 71 | 72 | func SetFont(newFont string) error { 73 | viper.Set("font", newFont) 74 | return viper.WriteConfig() 75 | } 76 | 77 | func GetLayout() int64 { 78 | return viper.GetInt64("layout") 79 | } 80 | 81 | func SetLayout(layoutType int64) error { 82 | viper.Set("layout", layoutType) 83 | return viper.WriteConfig() 84 | } 85 | -------------------------------------------------------------------------------- /frontend/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /frontend/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | // biome-ignore lint: disable 6 | export {} 7 | 8 | /* prettier-ignore */ 9 | declare module 'vue' { 10 | export interface GlobalComponents { 11 | CaptureDialog: typeof import('./src/components/CaptureDialog.vue')['default'] 12 | ElAlert: typeof import('element-plus/es')['ElAlert'] 13 | ElButton: typeof import('element-plus/es')['ElButton'] 14 | ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] 15 | ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] 16 | ElDialog: typeof import('element-plus/es')['ElDialog'] 17 | ElDropdown: typeof import('element-plus/es')['ElDropdown'] 18 | ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] 19 | ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 20 | ElInput: typeof import('element-plus/es')['ElInput'] 21 | ElOption: typeof import('element-plus/es')['ElOption'] 22 | ElSelect: typeof import('element-plus/es')['ElSelect'] 23 | ElTag: typeof import('element-plus/es')['ElTag'] 24 | ElTooltip: typeof import('element-plus/es')['ElTooltip'] 25 | PoolCard: typeof import('./src/components/PoolCard.vue')['default'] 26 | SettingDialog: typeof import('./src/components/SettingDialog.vue')['default'] 27 | } 28 | export interface ComponentCustomProperties { 29 | vLoading: typeof import('element-plus/es')['ElLoadingDirective'] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | gf2gacha 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc -b && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@element-plus/icons-vue": "^2.3.1", 13 | "@tailwindcss/vite": "^4.0.11", 14 | "element-plus": "^2.9.5", 15 | "html2canvas-pro": "^1.5.8", 16 | "pinia": "^3.0.1", 17 | "tailwindcss": "^4.0.11", 18 | "vue": "^3.5.13", 19 | "vue-echarts": "^7.0.3" 20 | }, 21 | "devDependencies": { 22 | "@vitejs/plugin-vue": "^5.2.1", 23 | "@vue/tsconfig": "^0.7.0", 24 | "typescript": "~5.7.2", 25 | "unplugin-auto-import": "^19.1.1", 26 | "unplugin-vue-components": "^28.4.1", 27 | "vite": "^6.2.0", 28 | "vue-tsc": "^2.2.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/package.json.md5: -------------------------------------------------------------------------------- 1 | da8c951790b55cdcd5fc4a2987b3eef9 -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 198 | 199 | 269 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/ChillRoundM.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatchaCabin/gf2gacha/98957835ff3b0b90afa5af33d84bb44ba4e4f34d/frontend/src/assets/fonts/ChillRoundM.otf -------------------------------------------------------------------------------- /frontend/src/components/CaptureDialog.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/PoolCard.vue: -------------------------------------------------------------------------------- 1 | 122 | 123 | -------------------------------------------------------------------------------- /frontend/src/components/SettingDialog.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import './style.css' 4 | import 'element-plus/theme-chalk/el-loading.css' 5 | import 'element-plus/theme-chalk/el-message-box.css' 6 | import App from './App.vue' 7 | 8 | const pinia = createPinia() 9 | createApp(App).use(pinia).mount('#app') 10 | -------------------------------------------------------------------------------- /frontend/src/stores/layout.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import {ref} from "vue"; 3 | 4 | export const useLayoutStore = defineStore('counter', () => { 5 | const layoutType = ref(0) 6 | 7 | return { layoutType } 8 | }) -------------------------------------------------------------------------------- /frontend/src/style.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | body { 4 | margin: 0; 5 | } 6 | 7 | @font-face { 8 | font-family: "ChillRoundM"; 9 | src: local(""), 10 | url("assets/fonts/ChillRoundM.otf"); 11 | } -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | 6 | /* Linting */ 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUncheckedSideEffectImports": true 12 | }, 13 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 14 | } 15 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import AutoImport from 'unplugin-auto-import/vite' 4 | import Components from 'unplugin-vue-components/vite' 5 | import {ElementPlusResolver} from 'unplugin-vue-components/resolvers' 6 | import tailwindcss from '@tailwindcss/vite' 7 | 8 | // https://vite.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | vue(), 12 | tailwindcss(), 13 | AutoImport({ 14 | resolvers: [ElementPlusResolver()], 15 | }), 16 | Components({ 17 | resolvers: [ElementPlusResolver()], 18 | }), 19 | ], 20 | }) 21 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/main/App.d.ts: -------------------------------------------------------------------------------- 1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 2 | // This file is automatically generated. DO NOT EDIT 3 | import {model} from '../models'; 4 | 5 | export function CaptureClose():Promise; 6 | 7 | export function CaptureStart():Promise; 8 | 9 | export function ExportMccExcel(arg1:string):Promise; 10 | 11 | export function ExportRawJson(arg1:string):Promise; 12 | 13 | export function GetCommunityExchangeList():Promise>; 14 | 15 | export function GetCurrentVersion():Promise; 16 | 17 | export function GetLatestVersion():Promise; 18 | 19 | export function GetLogInfo():Promise; 20 | 21 | export function GetPoolInfo(arg1:string,arg2:number):Promise; 22 | 23 | export function GetSettingExchangeList():Promise>; 24 | 25 | export function GetSettingFont():Promise; 26 | 27 | export function GetSettingLayout():Promise; 28 | 29 | export function GetUserList():Promise>; 30 | 31 | export function HandleCommunityTasks():Promise>; 32 | 33 | export function ImportMccExcel(arg1:string):Promise; 34 | 35 | export function ImportRawJson(arg1:string,arg2:boolean):Promise; 36 | 37 | export function MergeEreRecord(arg1:string,arg2:string):Promise; 38 | 39 | export function SaveSettingExchangeList(arg1:Array):Promise; 40 | 41 | export function SaveSettingFont(arg1:string):Promise; 42 | 43 | export function SaveSettingLayout(arg1:number):Promise; 44 | 45 | export function UpdatePoolInfo(arg1:boolean):Promise>; 46 | 47 | export function UpdateTo(arg1:string):Promise; 48 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/main/App.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | export function CaptureClose() { 6 | return window['go']['main']['App']['CaptureClose'](); 7 | } 8 | 9 | export function CaptureStart() { 10 | return window['go']['main']['App']['CaptureStart'](); 11 | } 12 | 13 | export function ExportMccExcel(arg1) { 14 | return window['go']['main']['App']['ExportMccExcel'](arg1); 15 | } 16 | 17 | export function ExportRawJson(arg1) { 18 | return window['go']['main']['App']['ExportRawJson'](arg1); 19 | } 20 | 21 | export function GetCommunityExchangeList() { 22 | return window['go']['main']['App']['GetCommunityExchangeList'](); 23 | } 24 | 25 | export function GetCurrentVersion() { 26 | return window['go']['main']['App']['GetCurrentVersion'](); 27 | } 28 | 29 | export function GetLatestVersion() { 30 | return window['go']['main']['App']['GetLatestVersion'](); 31 | } 32 | 33 | export function GetLogInfo() { 34 | return window['go']['main']['App']['GetLogInfo'](); 35 | } 36 | 37 | export function GetPoolInfo(arg1, arg2) { 38 | return window['go']['main']['App']['GetPoolInfo'](arg1, arg2); 39 | } 40 | 41 | export function GetSettingExchangeList() { 42 | return window['go']['main']['App']['GetSettingExchangeList'](); 43 | } 44 | 45 | export function GetSettingFont() { 46 | return window['go']['main']['App']['GetSettingFont'](); 47 | } 48 | 49 | export function GetSettingLayout() { 50 | return window['go']['main']['App']['GetSettingLayout'](); 51 | } 52 | 53 | export function GetUserList() { 54 | return window['go']['main']['App']['GetUserList'](); 55 | } 56 | 57 | export function HandleCommunityTasks() { 58 | return window['go']['main']['App']['HandleCommunityTasks'](); 59 | } 60 | 61 | export function ImportMccExcel(arg1) { 62 | return window['go']['main']['App']['ImportMccExcel'](arg1); 63 | } 64 | 65 | export function ImportRawJson(arg1, arg2) { 66 | return window['go']['main']['App']['ImportRawJson'](arg1, arg2); 67 | } 68 | 69 | export function MergeEreRecord(arg1, arg2) { 70 | return window['go']['main']['App']['MergeEreRecord'](arg1, arg2); 71 | } 72 | 73 | export function SaveSettingExchangeList(arg1) { 74 | return window['go']['main']['App']['SaveSettingExchangeList'](arg1); 75 | } 76 | 77 | export function SaveSettingFont(arg1) { 78 | return window['go']['main']['App']['SaveSettingFont'](arg1); 79 | } 80 | 81 | export function SaveSettingLayout(arg1) { 82 | return window['go']['main']['App']['SaveSettingLayout'](arg1); 83 | } 84 | 85 | export function UpdatePoolInfo(arg1) { 86 | return window['go']['main']['App']['UpdatePoolInfo'](arg1); 87 | } 88 | 89 | export function UpdateTo(arg1) { 90 | return window['go']['main']['App']['UpdateTo'](arg1); 91 | } 92 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/models.ts: -------------------------------------------------------------------------------- 1 | export namespace model { 2 | 3 | export class CommunityExchangeList { 4 | id: number; 5 | name: string; 6 | 7 | static createFrom(source: any = {}) { 8 | return new CommunityExchangeList(source); 9 | } 10 | 11 | constructor(source: any = {}) { 12 | if ('string' === typeof source) source = JSON.parse(source); 13 | this.id = source["id"]; 14 | this.name = source["name"]; 15 | } 16 | } 17 | export class DisplayRecord { 18 | id: number; 19 | name: string; 20 | lose: boolean; 21 | count: number; 22 | 23 | static createFrom(source: any = {}) { 24 | return new DisplayRecord(source); 25 | } 26 | 27 | constructor(source: any = {}) { 28 | if ('string' === typeof source) source = JSON.parse(source); 29 | this.id = source["id"]; 30 | this.name = source["name"]; 31 | this.lose = source["lose"]; 32 | this.count = source["count"]; 33 | } 34 | } 35 | export class LogInfo { 36 | tablePath: string; 37 | accessToken: string; 38 | uid: string; 39 | gachaUrl: string; 40 | 41 | static createFrom(source: any = {}) { 42 | return new LogInfo(source); 43 | } 44 | 45 | constructor(source: any = {}) { 46 | if ('string' === typeof source) source = JSON.parse(source); 47 | this.tablePath = source["tablePath"]; 48 | this.accessToken = source["accessToken"]; 49 | this.uid = source["uid"]; 50 | this.gachaUrl = source["gachaUrl"]; 51 | } 52 | } 53 | export class Pool { 54 | poolType: number; 55 | gachaCount: number; 56 | loseCount: number; 57 | guaranteesCount: number; 58 | rank5Count: number; 59 | rank4Count: number; 60 | rank3Count: number; 61 | storedCount: number; 62 | recordList: DisplayRecord[]; 63 | 64 | static createFrom(source: any = {}) { 65 | return new Pool(source); 66 | } 67 | 68 | constructor(source: any = {}) { 69 | if ('string' === typeof source) source = JSON.parse(source); 70 | this.poolType = source["poolType"]; 71 | this.gachaCount = source["gachaCount"]; 72 | this.loseCount = source["loseCount"]; 73 | this.guaranteesCount = source["guaranteesCount"]; 74 | this.rank5Count = source["rank5Count"]; 75 | this.rank4Count = source["rank4Count"]; 76 | this.rank3Count = source["rank3Count"]; 77 | this.storedCount = source["storedCount"]; 78 | this.recordList = this.convertValues(source["recordList"], DisplayRecord); 79 | } 80 | 81 | convertValues(a: any, classs: any, asMap: boolean = false): any { 82 | if (!a) { 83 | return a; 84 | } 85 | if (a.slice && a.map) { 86 | return (a as any[]).map(elem => this.convertValues(elem, classs)); 87 | } else if ("object" === typeof a) { 88 | if (asMap) { 89 | for (const key of Object.keys(a)) { 90 | a[key] = new classs(a[key]); 91 | } 92 | return a; 93 | } 94 | return new classs(a); 95 | } 96 | return a; 97 | } 98 | } 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /frontend/wailsjs/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wailsapp/runtime", 3 | "version": "2.0.0", 4 | "description": "Wails Javascript runtime library", 5 | "main": "runtime.js", 6 | "types": "runtime.d.ts", 7 | "scripts": { 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/wailsapp/wails.git" 12 | }, 13 | "keywords": [ 14 | "Wails", 15 | "Javascript", 16 | "Go" 17 | ], 18 | "author": "Lea Anthony ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wailsapp/wails/issues" 22 | }, 23 | "homepage": "https://github.com/wailsapp/wails#readme" 24 | } 25 | -------------------------------------------------------------------------------- /frontend/wailsjs/runtime/runtime.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | _ __ _ __ 3 | | | / /___ _(_) /____ 4 | | | /| / / __ `/ / / ___/ 5 | | |/ |/ / /_/ / / (__ ) 6 | |__/|__/\__,_/_/_/____/ 7 | The electron alternative for Go 8 | (c) Lea Anthony 2019-present 9 | */ 10 | 11 | export interface Position { 12 | x: number; 13 | y: number; 14 | } 15 | 16 | export interface Size { 17 | w: number; 18 | h: number; 19 | } 20 | 21 | export interface Screen { 22 | isCurrent: boolean; 23 | isPrimary: boolean; 24 | width : number 25 | height : number 26 | } 27 | 28 | // Environment information such as platform, buildtype, ... 29 | export interface EnvironmentInfo { 30 | buildType: string; 31 | platform: string; 32 | arch: string; 33 | } 34 | 35 | // [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) 36 | // emits the given event. Optional data may be passed with the event. 37 | // This will trigger any event listeners. 38 | export function EventsEmit(eventName: string, ...data: any): void; 39 | 40 | // [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. 41 | export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; 42 | 43 | // [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) 44 | // sets up a listener for the given event name, but will only trigger a given number times. 45 | export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; 46 | 47 | // [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) 48 | // sets up a listener for the given event name, but will only trigger once. 49 | export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; 50 | 51 | // [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) 52 | // unregisters the listener for the given event name. 53 | export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; 54 | 55 | // [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) 56 | // unregisters all listeners. 57 | export function EventsOffAll(): void; 58 | 59 | // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) 60 | // logs the given message as a raw message 61 | export function LogPrint(message: string): void; 62 | 63 | // [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) 64 | // logs the given message at the `trace` log level. 65 | export function LogTrace(message: string): void; 66 | 67 | // [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) 68 | // logs the given message at the `debug` log level. 69 | export function LogDebug(message: string): void; 70 | 71 | // [LogError](https://wails.io/docs/reference/runtime/log#logerror) 72 | // logs the given message at the `error` log level. 73 | export function LogError(message: string): void; 74 | 75 | // [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) 76 | // logs the given message at the `fatal` log level. 77 | // The application will quit after calling this method. 78 | export function LogFatal(message: string): void; 79 | 80 | // [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) 81 | // logs the given message at the `info` log level. 82 | export function LogInfo(message: string): void; 83 | 84 | // [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) 85 | // logs the given message at the `warning` log level. 86 | export function LogWarning(message: string): void; 87 | 88 | // [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) 89 | // Forces a reload by the main application as well as connected browsers. 90 | export function WindowReload(): void; 91 | 92 | // [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) 93 | // Reloads the application frontend. 94 | export function WindowReloadApp(): void; 95 | 96 | // [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) 97 | // Sets the window AlwaysOnTop or not on top. 98 | export function WindowSetAlwaysOnTop(b: boolean): void; 99 | 100 | // [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) 101 | // *Windows only* 102 | // Sets window theme to system default (dark/light). 103 | export function WindowSetSystemDefaultTheme(): void; 104 | 105 | // [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) 106 | // *Windows only* 107 | // Sets window to light theme. 108 | export function WindowSetLightTheme(): void; 109 | 110 | // [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) 111 | // *Windows only* 112 | // Sets window to dark theme. 113 | export function WindowSetDarkTheme(): void; 114 | 115 | // [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) 116 | // Centers the window on the monitor the window is currently on. 117 | export function WindowCenter(): void; 118 | 119 | // [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) 120 | // Sets the text in the window title bar. 121 | export function WindowSetTitle(title: string): void; 122 | 123 | // [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) 124 | // Makes the window full screen. 125 | export function WindowFullscreen(): void; 126 | 127 | // [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) 128 | // Restores the previous window dimensions and position prior to full screen. 129 | export function WindowUnfullscreen(): void; 130 | 131 | // [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) 132 | // Returns the state of the window, i.e. whether the window is in full screen mode or not. 133 | export function WindowIsFullscreen(): Promise; 134 | 135 | // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) 136 | // Sets the width and height of the window. 137 | export function WindowSetSize(width: number, height: number): void; 138 | 139 | // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) 140 | // Gets the width and height of the window. 141 | export function WindowGetSize(): Promise; 142 | 143 | // [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) 144 | // Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. 145 | // Setting a size of 0,0 will disable this constraint. 146 | export function WindowSetMaxSize(width: number, height: number): void; 147 | 148 | // [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) 149 | // Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. 150 | // Setting a size of 0,0 will disable this constraint. 151 | export function WindowSetMinSize(width: number, height: number): void; 152 | 153 | // [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) 154 | // Sets the window position relative to the monitor the window is currently on. 155 | export function WindowSetPosition(x: number, y: number): void; 156 | 157 | // [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) 158 | // Gets the window position relative to the monitor the window is currently on. 159 | export function WindowGetPosition(): Promise; 160 | 161 | // [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) 162 | // Hides the window. 163 | export function WindowHide(): void; 164 | 165 | // [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) 166 | // Shows the window, if it is currently hidden. 167 | export function WindowShow(): void; 168 | 169 | // [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) 170 | // Maximises the window to fill the screen. 171 | export function WindowMaximise(): void; 172 | 173 | // [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) 174 | // Toggles between Maximised and UnMaximised. 175 | export function WindowToggleMaximise(): void; 176 | 177 | // [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) 178 | // Restores the window to the dimensions and position prior to maximising. 179 | export function WindowUnmaximise(): void; 180 | 181 | // [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) 182 | // Returns the state of the window, i.e. whether the window is maximised or not. 183 | export function WindowIsMaximised(): Promise; 184 | 185 | // [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) 186 | // Minimises the window. 187 | export function WindowMinimise(): void; 188 | 189 | // [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) 190 | // Restores the window to the dimensions and position prior to minimising. 191 | export function WindowUnminimise(): void; 192 | 193 | // [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) 194 | // Returns the state of the window, i.e. whether the window is minimised or not. 195 | export function WindowIsMinimised(): Promise; 196 | 197 | // [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) 198 | // Returns the state of the window, i.e. whether the window is normal or not. 199 | export function WindowIsNormal(): Promise; 200 | 201 | // [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) 202 | // Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. 203 | export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; 204 | 205 | // [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) 206 | // Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. 207 | export function ScreenGetAll(): Promise; 208 | 209 | // [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) 210 | // Opens the given URL in the system browser. 211 | export function BrowserOpenURL(url: string): void; 212 | 213 | // [Environment](https://wails.io/docs/reference/runtime/intro#environment) 214 | // Returns information about the environment 215 | export function Environment(): Promise; 216 | 217 | // [Quit](https://wails.io/docs/reference/runtime/intro#quit) 218 | // Quits the application. 219 | export function Quit(): void; 220 | 221 | // [Hide](https://wails.io/docs/reference/runtime/intro#hide) 222 | // Hides the application. 223 | export function Hide(): void; 224 | 225 | // [Show](https://wails.io/docs/reference/runtime/intro#show) 226 | // Shows the application. 227 | export function Show(): void; 228 | 229 | // [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) 230 | // Returns the current text stored on clipboard 231 | export function ClipboardGetText(): Promise; 232 | 233 | // [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) 234 | // Sets a text on the clipboard 235 | export function ClipboardSetText(text: string): Promise; 236 | 237 | // [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) 238 | // OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. 239 | export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void 240 | 241 | // [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) 242 | // OnFileDropOff removes the drag and drop listeners and handlers. 243 | export function OnFileDropOff() :void 244 | 245 | // Check if the file path resolver is available 246 | export function CanResolveFilePaths(): boolean; 247 | 248 | // Resolves file paths for an array of files 249 | export function ResolveFilePaths(files: File[]): void -------------------------------------------------------------------------------- /frontend/wailsjs/runtime/runtime.js: -------------------------------------------------------------------------------- 1 | /* 2 | _ __ _ __ 3 | | | / /___ _(_) /____ 4 | | | /| / / __ `/ / / ___/ 5 | | |/ |/ / /_/ / / (__ ) 6 | |__/|__/\__,_/_/_/____/ 7 | The electron alternative for Go 8 | (c) Lea Anthony 2019-present 9 | */ 10 | 11 | export function LogPrint(message) { 12 | window.runtime.LogPrint(message); 13 | } 14 | 15 | export function LogTrace(message) { 16 | window.runtime.LogTrace(message); 17 | } 18 | 19 | export function LogDebug(message) { 20 | window.runtime.LogDebug(message); 21 | } 22 | 23 | export function LogInfo(message) { 24 | window.runtime.LogInfo(message); 25 | } 26 | 27 | export function LogWarning(message) { 28 | window.runtime.LogWarning(message); 29 | } 30 | 31 | export function LogError(message) { 32 | window.runtime.LogError(message); 33 | } 34 | 35 | export function LogFatal(message) { 36 | window.runtime.LogFatal(message); 37 | } 38 | 39 | export function EventsOnMultiple(eventName, callback, maxCallbacks) { 40 | return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); 41 | } 42 | 43 | export function EventsOn(eventName, callback) { 44 | return EventsOnMultiple(eventName, callback, -1); 45 | } 46 | 47 | export function EventsOff(eventName, ...additionalEventNames) { 48 | return window.runtime.EventsOff(eventName, ...additionalEventNames); 49 | } 50 | 51 | export function EventsOnce(eventName, callback) { 52 | return EventsOnMultiple(eventName, callback, 1); 53 | } 54 | 55 | export function EventsEmit(eventName) { 56 | let args = [eventName].slice.call(arguments); 57 | return window.runtime.EventsEmit.apply(null, args); 58 | } 59 | 60 | export function WindowReload() { 61 | window.runtime.WindowReload(); 62 | } 63 | 64 | export function WindowReloadApp() { 65 | window.runtime.WindowReloadApp(); 66 | } 67 | 68 | export function WindowSetAlwaysOnTop(b) { 69 | window.runtime.WindowSetAlwaysOnTop(b); 70 | } 71 | 72 | export function WindowSetSystemDefaultTheme() { 73 | window.runtime.WindowSetSystemDefaultTheme(); 74 | } 75 | 76 | export function WindowSetLightTheme() { 77 | window.runtime.WindowSetLightTheme(); 78 | } 79 | 80 | export function WindowSetDarkTheme() { 81 | window.runtime.WindowSetDarkTheme(); 82 | } 83 | 84 | export function WindowCenter() { 85 | window.runtime.WindowCenter(); 86 | } 87 | 88 | export function WindowSetTitle(title) { 89 | window.runtime.WindowSetTitle(title); 90 | } 91 | 92 | export function WindowFullscreen() { 93 | window.runtime.WindowFullscreen(); 94 | } 95 | 96 | export function WindowUnfullscreen() { 97 | window.runtime.WindowUnfullscreen(); 98 | } 99 | 100 | export function WindowIsFullscreen() { 101 | return window.runtime.WindowIsFullscreen(); 102 | } 103 | 104 | export function WindowGetSize() { 105 | return window.runtime.WindowGetSize(); 106 | } 107 | 108 | export function WindowSetSize(width, height) { 109 | window.runtime.WindowSetSize(width, height); 110 | } 111 | 112 | export function WindowSetMaxSize(width, height) { 113 | window.runtime.WindowSetMaxSize(width, height); 114 | } 115 | 116 | export function WindowSetMinSize(width, height) { 117 | window.runtime.WindowSetMinSize(width, height); 118 | } 119 | 120 | export function WindowSetPosition(x, y) { 121 | window.runtime.WindowSetPosition(x, y); 122 | } 123 | 124 | export function WindowGetPosition() { 125 | return window.runtime.WindowGetPosition(); 126 | } 127 | 128 | export function WindowHide() { 129 | window.runtime.WindowHide(); 130 | } 131 | 132 | export function WindowShow() { 133 | window.runtime.WindowShow(); 134 | } 135 | 136 | export function WindowMaximise() { 137 | window.runtime.WindowMaximise(); 138 | } 139 | 140 | export function WindowToggleMaximise() { 141 | window.runtime.WindowToggleMaximise(); 142 | } 143 | 144 | export function WindowUnmaximise() { 145 | window.runtime.WindowUnmaximise(); 146 | } 147 | 148 | export function WindowIsMaximised() { 149 | return window.runtime.WindowIsMaximised(); 150 | } 151 | 152 | export function WindowMinimise() { 153 | window.runtime.WindowMinimise(); 154 | } 155 | 156 | export function WindowUnminimise() { 157 | window.runtime.WindowUnminimise(); 158 | } 159 | 160 | export function WindowSetBackgroundColour(R, G, B, A) { 161 | window.runtime.WindowSetBackgroundColour(R, G, B, A); 162 | } 163 | 164 | export function ScreenGetAll() { 165 | return window.runtime.ScreenGetAll(); 166 | } 167 | 168 | export function WindowIsMinimised() { 169 | return window.runtime.WindowIsMinimised(); 170 | } 171 | 172 | export function WindowIsNormal() { 173 | return window.runtime.WindowIsNormal(); 174 | } 175 | 176 | export function BrowserOpenURL(url) { 177 | window.runtime.BrowserOpenURL(url); 178 | } 179 | 180 | export function Environment() { 181 | return window.runtime.Environment(); 182 | } 183 | 184 | export function Quit() { 185 | window.runtime.Quit(); 186 | } 187 | 188 | export function Hide() { 189 | window.runtime.Hide(); 190 | } 191 | 192 | export function Show() { 193 | window.runtime.Show(); 194 | } 195 | 196 | export function ClipboardGetText() { 197 | return window.runtime.ClipboardGetText(); 198 | } 199 | 200 | export function ClipboardSetText(text) { 201 | return window.runtime.ClipboardSetText(text); 202 | } 203 | 204 | /** 205 | * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. 206 | * 207 | * @export 208 | * @callback OnFileDropCallback 209 | * @param {number} x - x coordinate of the drop 210 | * @param {number} y - y coordinate of the drop 211 | * @param {string[]} paths - A list of file paths. 212 | */ 213 | 214 | /** 215 | * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. 216 | * 217 | * @export 218 | * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. 219 | * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) 220 | */ 221 | export function OnFileDrop(callback, useDropTarget) { 222 | return window.runtime.OnFileDrop(callback, useDropTarget); 223 | } 224 | 225 | /** 226 | * OnFileDropOff removes the drag and drop listeners and handlers. 227 | */ 228 | export function OnFileDropOff() { 229 | return window.runtime.OnFileDropOff(); 230 | } 231 | 232 | export function CanResolveFilePaths() { 233 | return window.runtime.CanResolveFilePaths(); 234 | } 235 | 236 | export function ResolveFilePaths(files) { 237 | return window.runtime.ResolveFilePaths(files); 238 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gf2gacha 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.5 6 | 7 | require ( 8 | github.com/elazarl/goproxy v1.7.2 9 | github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf 10 | github.com/pkg/errors v0.9.1 11 | github.com/spf13/viper v1.19.0 12 | github.com/wailsapp/wails/v2 v2.10.1 13 | github.com/xuri/excelize/v2 v2.9.0 14 | go.uber.org/zap v1.27.0 15 | golang.org/x/sys v0.33.0 16 | google.golang.org/protobuf v1.36.5 17 | modernc.org/sqlite v1.36.0 18 | xorm.io/xorm v1.3.9 19 | ) 20 | 21 | require ( 22 | github.com/bep/debounce v1.2.1 // indirect 23 | github.com/dustin/go-humanize v1.0.1 // indirect 24 | github.com/fsnotify/fsnotify v1.8.0 // indirect 25 | github.com/go-ole/go-ole v1.3.0 // indirect 26 | github.com/goccy/go-json v0.10.5 // indirect 27 | github.com/godbus/dbus/v5 v5.1.0 // indirect 28 | github.com/golang/snappy v1.0.0 // indirect 29 | github.com/google/uuid v1.6.0 // indirect 30 | github.com/hashicorp/hcl v1.0.0 // indirect 31 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/labstack/echo/v4 v4.13.3 // indirect 34 | github.com/labstack/gommon v0.4.2 // indirect 35 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect 36 | github.com/leaanthony/gosod v1.0.4 // indirect 37 | github.com/leaanthony/slicer v1.6.0 // indirect 38 | github.com/leaanthony/u v1.1.1 // indirect 39 | github.com/magiconair/properties v1.8.9 // indirect 40 | github.com/mattn/go-colorable v0.1.14 // indirect 41 | github.com/mattn/go-isatty v0.0.20 // indirect 42 | github.com/mitchellh/mapstructure v1.5.0 // indirect 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 44 | github.com/modern-go/reflect2 v1.0.2 // indirect 45 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 46 | github.com/ncruces/go-strftime v0.1.9 // indirect 47 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 48 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 49 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 50 | github.com/richardlehane/mscfb v1.0.4 // indirect 51 | github.com/richardlehane/msoleps v1.0.4 // indirect 52 | github.com/rivo/uniseg v0.4.7 // indirect 53 | github.com/sagikazarmark/locafero v0.7.0 // indirect 54 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 55 | github.com/samber/lo v1.49.1 // indirect 56 | github.com/sourcegraph/conc v0.3.0 // indirect 57 | github.com/spf13/afero v1.12.0 // indirect 58 | github.com/spf13/cast v1.7.1 // indirect 59 | github.com/spf13/pflag v1.0.6 // indirect 60 | github.com/subosito/gotenv v1.6.0 // indirect 61 | github.com/syndtr/goleveldb v1.0.0 // indirect 62 | github.com/tkrajina/go-reflector v0.5.8 // indirect 63 | github.com/valyala/bytebufferpool v1.0.0 // indirect 64 | github.com/valyala/fasttemplate v1.2.2 // indirect 65 | github.com/wailsapp/go-webview2 v1.0.19 // indirect 66 | github.com/wailsapp/mimetype v1.4.1 // indirect 67 | github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 // indirect 68 | github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba // indirect 69 | go.uber.org/multierr v1.11.0 // indirect 70 | golang.org/x/crypto v0.38.0 // indirect 71 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect 72 | golang.org/x/net v0.40.0 // indirect 73 | golang.org/x/text v0.25.0 // indirect 74 | gopkg.in/ini.v1 v1.67.0 // indirect 75 | gopkg.in/yaml.v3 v3.0.1 // indirect 76 | modernc.org/libc v1.61.13 // indirect 77 | modernc.org/mathutil v1.7.1 // indirect 78 | modernc.org/memory v1.8.2 // indirect 79 | xorm.io/builder v0.3.13 // indirect 80 | ) 81 | 82 | // replace github.com/wailsapp/wails/v2 v2.9.1 => D:\Project\Go\pkg\mod 83 | -------------------------------------------------------------------------------- /logger/init.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | "io" 7 | "os" 8 | ) 9 | 10 | var Logger *zap.SugaredLogger 11 | 12 | func init() { 13 | logFile, err := os.OpenFile("gf2gacha.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0755) 14 | if err != nil { 15 | panic(err) 16 | } 17 | mw := io.MultiWriter(logFile, os.Stdout) 18 | mwSyncer := zapcore.AddSync(mw) 19 | 20 | encoderConfig := zap.NewProductionEncoderConfig() 21 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 22 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 23 | encoder := zapcore.NewConsoleEncoder(encoderConfig) 24 | 25 | core := zapcore.NewCore(encoder, mwSyncer, zapcore.InfoLevel) 26 | Logger = zap.New(core).Sugar() 27 | } 28 | -------------------------------------------------------------------------------- /logic/AppendLog.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func AppendLog(accessToken string, uid int64, gachaUrl string) error { 11 | userHome, err := os.UserHomeDir() 12 | if err != nil { 13 | return errors.WithStack(err) 14 | } 15 | 16 | logPath := filepath.Join(userHome, "/AppData/LocalLow/SunBorn/少女前线2:追放/Player.log") 17 | file, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND, 0644) 18 | if err != nil { 19 | return err 20 | } 21 | defer file.Close() 22 | 23 | _, err = file.WriteString(fmt.Sprintf(`{"access_token":"%s","uid":%d}\n`, accessToken, uid)) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | _, err = file.WriteString(fmt.Sprintf(`{"gacha_record_url":"%s"}\n`, gachaUrl)) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /logic/ExportRecord.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gf2gacha/logger" 7 | "gf2gacha/model" 8 | "gf2gacha/preload" 9 | "github.com/pkg/errors" 10 | "github.com/xuri/excelize/v2" 11 | "os" 12 | "path/filepath" 13 | "time" 14 | ) 15 | 16 | func ExportRawJson(uid, saveDir string) error { 17 | rawMap := make(map[int64][]model.RemoteRecord) 18 | for _, poolTypeUnit := range preload.PoolTypeMap { 19 | localRecordList, err := GetLocalRecord(uid, poolTypeUnit.Id, 0) 20 | if err != nil { 21 | return errors.WithStack(err) 22 | } 23 | for _, record := range localRecordList { 24 | rawMap[poolTypeUnit.Id] = append(rawMap[poolTypeUnit.Id], model.RemoteRecord{ 25 | PoolId: record.PoolId, 26 | ItemId: record.ItemId, 27 | GachaTimestamp: record.GachaTimestamp, 28 | }) 29 | } 30 | } 31 | rawJsonBytes, err := json.MarshalIndent(&rawMap, "", "\t") 32 | if err != nil { 33 | return errors.WithStack(err) 34 | } 35 | 36 | err = os.WriteFile(filepath.Join(saveDir, fmt.Sprintf("gf2gacha-raw-%s_%s.json", uid, time.Now().Format("20060102150405"))), rawJsonBytes, 0755) 37 | if err != nil { 38 | return errors.WithStack(err) 39 | } 40 | return nil 41 | } 42 | 43 | func ExportMccExcel(uid, saveDir string) error { 44 | excel := excelize.NewFile() 45 | defer excel.Close() 46 | 47 | styleTitle, err := excel.NewStyle(&excelize.Style{ 48 | Font: &excelize.Font{ 49 | Family: "Microsoft YaHei", 50 | Size: 14, 51 | Bold: true, 52 | }, 53 | Alignment: &excelize.Alignment{ 54 | Horizontal: "left", 55 | }, 56 | }) 57 | if err != nil { 58 | return errors.WithStack(err) 59 | } 60 | 61 | styleRank3, err := excel.NewStyle(&excelize.Style{ 62 | Font: &excelize.Font{ 63 | Family: "Microsoft YaHei", 64 | Size: 14, 65 | Color: "#5B9BD5", 66 | }, 67 | Alignment: &excelize.Alignment{ 68 | Horizontal: "left", 69 | }, 70 | }) 71 | if err != nil { 72 | return errors.WithStack(err) 73 | } 74 | 75 | styleRank4, err := excel.NewStyle(&excelize.Style{ 76 | Font: &excelize.Font{ 77 | Family: "Microsoft YaHei", 78 | Size: 14, 79 | Color: "#CC00CC", 80 | }, 81 | Alignment: &excelize.Alignment{ 82 | Horizontal: "left", 83 | }, 84 | }) 85 | if err != nil { 86 | return errors.WithStack(err) 87 | } 88 | 89 | styleRank5, err := excel.NewStyle(&excelize.Style{ 90 | Font: &excelize.Font{ 91 | Family: "Microsoft YaHei", 92 | Size: 14, 93 | Color: "#FFC000", 94 | }, 95 | Alignment: &excelize.Alignment{ 96 | Horizontal: "left", 97 | }, 98 | }) 99 | if err != nil { 100 | return errors.WithStack(err) 101 | } 102 | 103 | for _, poolTypeUnit := range preload.PoolTypeMap { 104 | localRecordList, err := GetLocalRecord(uid, poolTypeUnit.Id, 0) 105 | if err != nil { 106 | return errors.WithStack(err) 107 | } 108 | if len(localRecordList) > 0 { 109 | var sheetName string 110 | switch poolTypeUnit.Id { 111 | case 1: 112 | sheetName = "常规采购" 113 | case 3: 114 | sheetName = "定向采购" 115 | case 4: 116 | sheetName = "军备提升" 117 | case 5: 118 | sheetName = "初始采购" 119 | case 6: 120 | sheetName = "自选人形" 121 | case 7: 122 | sheetName = "自选武器" 123 | case 8: 124 | sheetName = "神秘箱" 125 | default: 126 | logger.Logger.Warnf("未定义的卡池 poolType:%d poolName:%s", poolTypeUnit.Id, poolTypeUnit.Name) 127 | continue 128 | } 129 | _, err = excel.NewSheet(sheetName) 130 | if err != nil { 131 | return errors.WithStack(err) 132 | } 133 | 134 | err = excel.SetSheetRow(sheetName, "A1", &[]string{"卡池Id", "抽卡时间", "道具Id", "道具名称"}) 135 | if err != nil { 136 | return errors.WithStack(err) 137 | } 138 | 139 | err = excel.SetRowStyle(sheetName, 1, 1, styleTitle) 140 | if err != nil { 141 | return errors.WithStack(err) 142 | } 143 | err = excel.SetColWidth(sheetName, "A", "A", 10) 144 | if err != nil { 145 | return errors.WithStack(err) 146 | } 147 | err = excel.SetColWidth(sheetName, "B", "B", 36) 148 | if err != nil { 149 | return errors.WithStack(err) 150 | } 151 | err = excel.SetColWidth(sheetName, "C", "C", 12) 152 | if err != nil { 153 | return errors.WithStack(err) 154 | } 155 | err = excel.SetColWidth(sheetName, "D", "D", 42) 156 | if err != nil { 157 | return errors.WithStack(err) 158 | } 159 | 160 | for i, record := range localRecordList { 161 | rowIndex := i + 2 162 | cellName := fmt.Sprintf("A%d", rowIndex) 163 | poolId := record.PoolId 164 | gachaTime := time.Unix(record.GachaTimestamp, 0).Format("2006年01月02日 15:04:05") 165 | itemId := record.ItemId 166 | itemName := preload.LangMap[preload.ItemMap[itemId].Name.Id] 167 | err = excel.SetSheetRow(sheetName, cellName, &[]interface{}{poolId, gachaTime, itemId, itemName}) 168 | if err != nil { 169 | return errors.WithStack(err) 170 | } 171 | 172 | itemRank := preload.ItemRankMap[poolId][itemId] 173 | switch itemRank { 174 | case 3: 175 | err = excel.SetRowStyle(sheetName, rowIndex, rowIndex, styleRank3) 176 | if err != nil { 177 | return errors.WithStack(err) 178 | } 179 | case 4: 180 | err = excel.SetRowStyle(sheetName, rowIndex, rowIndex, styleRank4) 181 | if err != nil { 182 | return errors.WithStack(err) 183 | } 184 | case 5: 185 | err = excel.SetRowStyle(sheetName, rowIndex, rowIndex, styleRank5) 186 | if err != nil { 187 | return errors.WithStack(err) 188 | } 189 | default: 190 | logger.Logger.Warnf("未知道具等级%d %+v", itemRank, record) 191 | } 192 | } 193 | } 194 | } 195 | 196 | err = excel.DeleteSheet("Sheet1") 197 | if err != nil { 198 | return errors.WithStack(err) 199 | } 200 | 201 | err = excel.SaveAs(filepath.Join(saveDir, fmt.Sprintf("gf2gacha-excel-%s_%s.xlsx", uid, time.Now().Format("20060102150405")))) 202 | if err != nil { 203 | return errors.WithStack(err) 204 | } 205 | 206 | return nil 207 | } 208 | -------------------------------------------------------------------------------- /logic/FetchRemoteData.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "encoding/json" 5 | "gf2gacha/model" 6 | "github.com/pkg/errors" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | func FetchRemoteData(gachaUrl, accessToken, next string, poolType int64) (data model.ResponseData, err error) { 15 | reqBody := url.Values{} 16 | if next != "" { 17 | reqBody.Set("next", next) 18 | } 19 | reqBody.Set("type_id", strconv.FormatInt(poolType, 10)) 20 | 21 | request, err := http.NewRequest("POST", gachaUrl, strings.NewReader(reqBody.Encode())) 22 | if err != nil { 23 | return model.ResponseData{}, errors.WithStack(err) 24 | } 25 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 26 | request.Header.Set("Authorization", accessToken) 27 | 28 | resp, err := http.DefaultClient.Do(request) 29 | if err != nil { 30 | return model.ResponseData{}, errors.WithStack(err) 31 | } 32 | defer resp.Body.Close() 33 | 34 | respBodyBytes, err := io.ReadAll(resp.Body) 35 | if err != nil { 36 | return model.ResponseData{}, errors.WithStack(err) 37 | } 38 | var respBody model.ResponseBody 39 | err = json.Unmarshal(respBodyBytes, &respBody) 40 | if err != nil { 41 | return model.ResponseData{}, errors.WithStack(err) 42 | } 43 | if respBody.Code != 0 { 44 | return model.ResponseData{}, errors.Errorf("%s(Code %d)", respBody.Message, respBody.Code) 45 | } 46 | 47 | err = json.Unmarshal(respBody.Data, &data) 48 | if err != nil { 49 | return model.ResponseData{}, errors.WithStack(err) 50 | } 51 | return data, nil 52 | } 53 | -------------------------------------------------------------------------------- /logic/GetCommunityExchangeList.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "fmt" 5 | "gf2gacha/model" 6 | "gf2gacha/request" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func GetCommunityExchangeList() ([]model.CommunityExchangeList, error) { 11 | exchangeListData, err := request.CommunityExchangeList("") 12 | if err != nil { 13 | return nil, errors.WithStack(err) 14 | } 15 | 16 | var exchangeList []model.CommunityExchangeList 17 | for _, exchangeItem := range exchangeListData.List { 18 | exchangeList = append(exchangeList, model.CommunityExchangeList{ 19 | Id: exchangeItem.ExchangeId, 20 | Name: fmt.Sprintf("『%s*%d』x%d", exchangeItem.ItemName, exchangeItem.ItemCount, exchangeItem.MaxExchangeCount), 21 | }) 22 | } 23 | return exchangeList, nil 24 | } 25 | -------------------------------------------------------------------------------- /logic/GetLatestVersion.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | func GetLatestVersion() (string, error) { 10 | resp, err := http.Get("https://gfl2worker.mcc.wiki/gf2gacha/version") 11 | if err != nil { 12 | return "", err 13 | } 14 | defer resp.Body.Close() 15 | 16 | if resp.StatusCode != 200 { 17 | return "", errors.New(resp.Status) 18 | } 19 | 20 | bodyBytes, err := io.ReadAll(resp.Body) 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | return string(bodyBytes), nil 26 | } 27 | -------------------------------------------------------------------------------- /logic/GetLocalRecord.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "gf2gacha/model" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func GetLocalRecord(uid string, poolType, endTimestamp int64) (recordList []model.LocalRecord, err error) { 9 | if uid == "" { 10 | return nil, errors.New("uid为空") 11 | } 12 | 13 | session := model.Engine.NewSession() 14 | defer session.Close() 15 | 16 | err = session.Table(uid).Sync(new(model.LocalRecord)) 17 | if err != nil { 18 | return nil, errors.WithStack(err) 19 | } 20 | 21 | if endTimestamp != 0 { 22 | err = session.Table(uid).Where("gacha_timestamp exchangeListData.List[j].UseScore 196 | }) 197 | for _, exchangeItem := range exchangeListData.List { 198 | //如果不在用户设置里,那就跳过 199 | if _, has := exchangeMap[exchangeItem.ExchangeId]; !has { 200 | messageList = append(messageList, fmt.Sprintf("用户设置不兑换『%s*%d』", exchangeItem.ItemName, exchangeItem.ItemCount)) 201 | continue 202 | } 203 | if exchangeItem.ExchangeCount < exchangeItem.MaxExchangeCount { 204 | for i := int64(0); i < exchangeItem.MaxExchangeCount-exchangeItem.ExchangeCount; i++ { 205 | info, err := request.CommunityUserInfo(webToken) 206 | if err != nil { 207 | return nil, errors.WithStack(err) 208 | } 209 | 210 | if info.User.Score >= exchangeItem.UseScore { 211 | err = request.CommunityExchange(webToken, exchangeItem.ExchangeId) 212 | if err != nil { 213 | return nil, errors.WithStack(err) 214 | } 215 | messageList = append(messageList, fmt.Sprintf("消耗积分%d,成功兑换『%s*%d』", exchangeItem.UseScore, exchangeItem.ItemName, exchangeItem.ItemCount)) 216 | } else { 217 | messageList = append(messageList, fmt.Sprintf("积分不足%d,无法兑换『%s*%d』", exchangeItem.UseScore, exchangeItem.ItemName, exchangeItem.ItemCount)) 218 | } 219 | } 220 | } 221 | } 222 | 223 | return messageList, nil 224 | } 225 | -------------------------------------------------------------------------------- /logic/ImportRecord.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "encoding/json" 5 | "gf2gacha/logger" 6 | "gf2gacha/model" 7 | "gf2gacha/util" 8 | "github.com/pkg/errors" 9 | "github.com/xuri/excelize/v2" 10 | "os" 11 | "slices" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | func ImportRawJson(uid, rawJsonPath string, isReverse bool) error { 17 | rawJsonBytes, err := os.ReadFile(rawJsonPath) 18 | if err != nil { 19 | return errors.WithStack(err) 20 | } 21 | 22 | rawMap := make(map[int64][]model.RemoteRecord) 23 | err = json.Unmarshal(rawJsonBytes, &rawMap) 24 | if err != nil { 25 | return errors.WithStack(err) 26 | } 27 | 28 | //解析成功后先做备份 29 | err = util.BackupDB() 30 | if err != nil { 31 | return errors.WithStack(err) 32 | } 33 | 34 | for poolType, rawRecordList := range rawMap { 35 | var recordList []model.LocalRecord 36 | if isReverse { 37 | slices.Reverse(rawRecordList) 38 | } 39 | for _, record := range rawRecordList { 40 | recordList = append(recordList, model.LocalRecord{ 41 | PoolType: poolType, 42 | PoolId: record.PoolId, 43 | ItemId: record.ItemId, 44 | GachaTimestamp: record.GachaTimestamp, 45 | }) 46 | } 47 | 48 | localRecordList, err := GetLocalRecord(uid, poolType, 0) 49 | if err != nil { 50 | return errors.WithStack(err) 51 | } 52 | 53 | newRecordList := MergeRecord(localRecordList, recordList) 54 | if len(newRecordList) == 0 { 55 | return nil 56 | } 57 | 58 | err = RemoveLocalRecord(uid, poolType) 59 | if err != nil { 60 | return errors.WithStack(err) 61 | } 62 | 63 | err = SaveLocalRecord(uid, newRecordList) 64 | if err != nil { 65 | return errors.WithStack(err) 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func ImportMccExcel(uid, excelPath string) error { 73 | excel, err := excelize.OpenFile(excelPath) 74 | if err != nil { 75 | return errors.WithStack(err) 76 | } 77 | defer excel.Close() 78 | 79 | poolMap := make(map[int64][]model.LocalRecord) 80 | for _, sheetName := range excel.GetSheetList() { 81 | var poolType int64 82 | switch sheetName { 83 | case "常规采购": 84 | poolType = 1 85 | case "定向采购": 86 | poolType = 3 87 | case "军备提升": 88 | poolType = 4 89 | case "初始采购": 90 | poolType = 5 91 | case "自选人形": 92 | poolType = 6 93 | case "自选武器": 94 | poolType = 7 95 | case "神秘箱": 96 | poolType = 8 97 | default: 98 | logger.Logger.Warnf("未知Sheet名称:%s", sheetName) 99 | continue 100 | } 101 | rows, err := excel.GetRows(sheetName) 102 | if err != nil { 103 | errors.WithStack(err) 104 | } 105 | 106 | fieldMapping := make(map[string]int) 107 | for rowIndex, row := range rows { 108 | //先获取标题栏索引值,以防止有乱序的情况 109 | if rowIndex == 0 { 110 | for colIndex, cell := range row { 111 | switch cell { 112 | case "卡池Id": 113 | fieldMapping["PoolId"] = colIndex 114 | case "抽卡时间": 115 | fieldMapping["GachaTime"] = colIndex 116 | case "道具Id": 117 | fieldMapping["ItemId"] = colIndex 118 | } 119 | } 120 | continue 121 | } 122 | 123 | var poolId, itemId, gachaTimestamp int64 124 | poolIdIndex := fieldMapping["PoolId"] 125 | if poolIdIndex >= len(row) { 126 | return errors.Errorf("%s第%d行:卡池Id(Index:%d)越界或不存在", sheetName, rowIndex, poolIdIndex) 127 | } 128 | poolId, err = strconv.ParseInt(row[poolIdIndex], 10, 64) 129 | if err != nil { 130 | return errors.WithStack(err) 131 | } 132 | 133 | gachaTimeIndex := fieldMapping["GachaTime"] 134 | if gachaTimeIndex >= len(row) { 135 | return errors.Errorf("%s第%d行:抽卡时间(Index:%d)越界或不存在", sheetName, rowIndex, gachaTimeIndex) 136 | } 137 | 138 | t, err := time.ParseInLocation("2006年01月02日 15:04:05", row[gachaTimeIndex], time.Local) 139 | if err != nil { 140 | return errors.WithStack(err) 141 | } 142 | gachaTimestamp = t.Unix() 143 | 144 | itemIdIndex := fieldMapping["ItemId"] 145 | if itemIdIndex >= len(row) { 146 | return errors.Errorf("%s第%d行:道具Id(Index:%d)越界或不存在", sheetName, rowIndex, itemIdIndex) 147 | } 148 | itemId, err = strconv.ParseInt(row[itemIdIndex], 10, 64) 149 | if err != nil { 150 | return errors.WithStack(err) 151 | } 152 | 153 | poolMap[poolType] = append(poolMap[poolType], model.LocalRecord{ 154 | PoolType: poolType, 155 | PoolId: poolId, 156 | ItemId: itemId, 157 | GachaTimestamp: gachaTimestamp, 158 | }) 159 | } 160 | } 161 | 162 | if len(poolMap) == 0 { 163 | return errors.New("Excel内无可解析数据") 164 | } 165 | 166 | //解析成功后先做备份 167 | err = util.BackupDB() 168 | if err != nil { 169 | return errors.WithStack(err) 170 | } 171 | 172 | for poolType, recordList := range poolMap { 173 | localRecordList, err := GetLocalRecord(uid, poolType, 0) 174 | if err != nil { 175 | return errors.WithStack(err) 176 | } 177 | 178 | newRecordList := MergeRecord(localRecordList, recordList) 179 | if len(newRecordList) == 0 { 180 | return nil 181 | } 182 | 183 | err = RemoveLocalRecord(uid, poolType) 184 | if err != nil { 185 | return errors.WithStack(err) 186 | } 187 | 188 | err = SaveLocalRecord(uid, newRecordList) 189 | if err != nil { 190 | return errors.WithStack(err) 191 | } 192 | } 193 | 194 | return nil 195 | } 196 | -------------------------------------------------------------------------------- /logic/MergeEreRecord.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "gf2gacha/model" 5 | "gf2gacha/util" 6 | "github.com/pkg/errors" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func MergeEreRecord(uid, erePath, typ string) error { 12 | ereFileData, err := os.ReadFile(erePath) 13 | if err != nil { 14 | return errors.WithStack(err) 15 | } 16 | 17 | var ereRecordList []model.LocalRecord 18 | 19 | switch strings.ToLower(typ) { 20 | case "json": 21 | ereRecordList, err = ParseEreJsonData(ereFileData) 22 | if err != nil { 23 | return errors.WithStack(err) 24 | } 25 | case "excel": 26 | ereRecordList, err = ParseEreExcelData(ereFileData) 27 | if err != nil { 28 | return errors.WithStack(err) 29 | } 30 | default: 31 | return errors.Errorf("unknown ere type: %s", typ) 32 | } 33 | 34 | //解析成功后先做备份 35 | err = util.BackupDB() 36 | if err != nil { 37 | return errors.WithStack(err) 38 | } 39 | 40 | //需要每个池子分开处理 41 | poolMap := make(map[int64][]model.LocalRecord) 42 | for i, record := range ereRecordList { 43 | poolMap[record.PoolType] = append(poolMap[record.PoolType], ereRecordList[i]) 44 | } 45 | 46 | for poolType, recordList := range poolMap { 47 | err = mergeEreRecord(uid, poolType, recordList) 48 | if err != nil { 49 | return errors.WithStack(err) 50 | } 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func mergeEreRecord(uid string, poolType int64, ereRecordList []model.LocalRecord) error { 57 | localRecordList, err := GetLocalRecord(uid, poolType, 0) 58 | if err != nil { 59 | return errors.WithStack(err) 60 | } 61 | 62 | newRecordList := MergeRecord(localRecordList, ereRecordList) 63 | if len(newRecordList) == 0 { 64 | return nil 65 | } 66 | 67 | err = RemoveLocalRecord(uid, poolType) 68 | if err != nil { 69 | return errors.WithStack(err) 70 | } 71 | 72 | err = SaveLocalRecord(uid, newRecordList) 73 | if err != nil { 74 | return errors.WithStack(err) 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /logic/MergeRecord.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "cmp" 5 | "fmt" 6 | "gf2gacha/logger" 7 | "gf2gacha/model" 8 | "slices" 9 | "strconv" 10 | ) 11 | 12 | func MergeRecord(recordList1, recordList2 []model.LocalRecord) (newRecordList []model.LocalRecord) { 13 | recordList1 = addFakeId(recordList1) 14 | recordList2 = addFakeId(recordList2) 15 | tmpRecordList := append(recordList1, recordList2...) 16 | slices.SortStableFunc(tmpRecordList, func(a, b model.LocalRecord) int { 17 | return cmp.Compare(a.Id, b.Id) 18 | }) 19 | 20 | fakeIdMap := make(map[int64]struct{}) 21 | for _, record := range tmpRecordList { 22 | if _, exist := fakeIdMap[record.Id]; !exist { 23 | fakeIdMap[record.Id] = struct{}{} 24 | newRecordList = append(newRecordList, model.LocalRecord{ 25 | PoolType: record.PoolType, 26 | PoolId: record.PoolId, 27 | ItemId: record.ItemId, 28 | GachaTimestamp: record.GachaTimestamp, 29 | }) 30 | } 31 | } 32 | 33 | return newRecordList 34 | } 35 | 36 | func addFakeId(recordList []model.LocalRecord) []model.LocalRecord { 37 | if len(recordList) == 0 { 38 | return nil 39 | } 40 | var preTimeStamp, order int64 41 | for i, record := range recordList { 42 | if record.GachaTimestamp != preTimeStamp { 43 | //前一个只能是0或9 44 | if order != 0 && order != 9 { 45 | logger.Logger.Warnf("存在抽卡时间相同但不足十连的数据:%+v", recordList[i-1]) 46 | } 47 | order = 0 48 | } else { 49 | order++ 50 | if order > 9 { 51 | logger.Logger.Warnf("存在抽卡时间相同但超过十连的数据:%+v", recordList[i-1]) 52 | } 53 | } 54 | 55 | fakeId, _ := strconv.ParseInt(fmt.Sprintf("%d%02d", record.GachaTimestamp, order), 10, 64) 56 | recordList[i].Id = fakeId 57 | 58 | preTimeStamp = record.GachaTimestamp 59 | } 60 | return recordList 61 | } 62 | -------------------------------------------------------------------------------- /logic/ParseEreExcelData.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "bytes" 5 | "gf2gacha/logger" 6 | "gf2gacha/model" 7 | "gf2gacha/preload" 8 | "github.com/pkg/errors" 9 | "github.com/xuri/excelize/v2" 10 | "strconv" 11 | ) 12 | 13 | func ParseEreExcelData(ereExcelData []byte) ([]model.LocalRecord, error) { 14 | reader, err := excelize.OpenReader(bytes.NewReader(ereExcelData)) 15 | if err != nil { 16 | return nil, errors.WithStack(err) 17 | } 18 | defer reader.Close() 19 | 20 | var recordList []model.LocalRecord 21 | for _, sheetName := range reader.GetSheetList() { 22 | var poolType int64 23 | switch sheetName { 24 | case "常规采购": 25 | poolType = 1 26 | case "定向采购": 27 | poolType = 3 28 | case "军备提升": 29 | poolType = 4 30 | case "初始采购": 31 | poolType = 5 32 | default: 33 | logger.Logger.Warnf("未知Sheet名称:%s", sheetName) 34 | continue 35 | } 36 | rows, err := reader.GetRows(sheetName) 37 | if err != nil { 38 | return nil, errors.WithStack(err) 39 | } 40 | 41 | fieldMapping := make(map[string]int) 42 | for rowIndex, row := range rows { 43 | //先获取标题栏索引值,以防止有乱序的情况 44 | if rowIndex == 0 { 45 | for colIndex, cell := range row { 46 | switch cell { 47 | case "时间": 48 | fieldMapping["GachaTimestamp"] = colIndex 49 | case "备注": 50 | fieldMapping["PoolId"] = colIndex 51 | case "类别": 52 | fieldMapping["ItemType"] = colIndex 53 | case "名称": 54 | fieldMapping["ItemName"] = colIndex 55 | } 56 | } 57 | continue 58 | } 59 | 60 | var poolId, itemId, gachaTimestamp int64 61 | var itemType, itemName string 62 | poolIdIndex := fieldMapping["PoolId"] 63 | if poolIdIndex >= len(row) { 64 | return nil, errors.Errorf("%s第%d行:备注(Index:%d)越界", sheetName, rowIndex, poolIdIndex) 65 | } 66 | poolId, err = strconv.ParseInt(row[poolIdIndex], 10, 64) 67 | if err != nil { 68 | return nil, errors.WithStack(err) 69 | } 70 | 71 | gachaTimestampIndex := fieldMapping["GachaTimestamp"] 72 | if gachaTimestampIndex >= len(row) { 73 | return nil, errors.Errorf("%s第%d行:时间(Index:%d)越界", sheetName, rowIndex, gachaTimestampIndex) 74 | } 75 | gachaTimestamp, err = strconv.ParseInt(row[gachaTimestampIndex], 10, 64) 76 | if err != nil { 77 | return nil, errors.WithStack(err) 78 | } 79 | 80 | itemTypeIndex := fieldMapping["ItemType"] 81 | if itemTypeIndex >= len(row) { 82 | return nil, errors.Errorf("%s第%d行:类别(Index:%d)越界", sheetName, rowIndex, itemTypeIndex) 83 | } 84 | itemType = row[itemTypeIndex] 85 | 86 | itemNameIndex := fieldMapping["ItemName"] 87 | if itemNameIndex >= len(row) { 88 | return nil, errors.Errorf("%s第%d行:名称(Index:%d)越界", sheetName, rowIndex, itemNameIndex) 89 | } 90 | itemName = row[itemNameIndex] 91 | 92 | switch itemType { 93 | case "角色": 94 | itemId = preload.DollNameMapping[itemName] 95 | case "武器": 96 | itemId = preload.WeaponNameMapping[itemName] 97 | default: 98 | return nil, errors.Errorf("%s第%d行:未知类别(Index:%d):%s", sheetName, rowIndex, itemTypeIndex, itemType) 99 | } 100 | recordList = append(recordList, model.LocalRecord{ 101 | PoolType: poolType, 102 | PoolId: poolId, 103 | ItemId: itemId, 104 | GachaTimestamp: gachaTimestamp, 105 | }) 106 | } 107 | } 108 | 109 | return recordList, nil 110 | } 111 | -------------------------------------------------------------------------------- /logic/ParseEreJsonData.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "encoding/json" 5 | "gf2gacha/model" 6 | "github.com/pkg/errors" 7 | "strconv" 8 | ) 9 | 10 | type ereJsonStruct struct { 11 | Result [][]interface{} `json:"result"` 12 | Time int64 `json:"time"` 13 | TypeMap [][]string `json:"typeMap"` 14 | Account string `json:"account"` 15 | } 16 | 17 | func ParseEreJsonData(ereJsonData []byte) ([]model.LocalRecord, error) { 18 | var ereData ereJsonStruct 19 | err := json.Unmarshal(ereJsonData, &ereData) 20 | if err != nil { 21 | return nil, errors.WithStack(err) 22 | } 23 | 24 | var recordList []model.LocalRecord 25 | for _, result := range ereData.Result { 26 | poolType, err := strconv.ParseInt(result[0].(string), 10, 64) 27 | if err != nil { 28 | return nil, errors.WithStack(err) 29 | } 30 | 31 | for _, ereUnit := range result[1].([]interface{}) { 32 | ereUnitArray := ereUnit.([]interface{}) 33 | recordList = append(recordList, model.LocalRecord{ 34 | PoolType: poolType, 35 | PoolId: int64(ereUnitArray[4].(float64)), 36 | ItemId: int64(ereUnitArray[0].(float64)), 37 | GachaTimestamp: int64(ereUnitArray[5].(float64)), 38 | }) 39 | } 40 | } 41 | 42 | return recordList, nil 43 | } 44 | -------------------------------------------------------------------------------- /logic/RemoveLocalRecord.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "gf2gacha/model" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func RemoveLocalRecord(uid string, poolType int64) (err error) { 9 | session := model.Engine.NewSession() 10 | defer session.Close() 11 | 12 | _, err = session.Table(uid).Delete(&model.LocalRecord{PoolType: poolType}) 13 | if err != nil { 14 | return errors.WithStack(err) 15 | } 16 | 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /logic/SaveLocalRecord.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "gf2gacha/model" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func SaveLocalRecord(uid string, recordList []model.LocalRecord) error { 9 | if uid == "" { 10 | return errors.New("uid为空") 11 | } 12 | 13 | session := model.Engine.NewSession() 14 | defer session.Close() 15 | 16 | err := session.Table(uid).Sync(new(model.LocalRecord)) 17 | if err != nil { 18 | return errors.WithStack(err) 19 | } 20 | 21 | for len(recordList) > 0 { 22 | maxCap := min(100, len(recordList)) 23 | _, err = session.Table(uid).Insert(recordList[:maxCap]) 24 | if err != nil { 25 | return errors.WithStack(err) 26 | } 27 | recordList = recordList[maxCap:] 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /logic/UpdatePoolInfo.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "fmt" 5 | "gf2gacha/logger" 6 | "gf2gacha/model" 7 | "gf2gacha/preload" 8 | "gf2gacha/util" 9 | "github.com/pkg/errors" 10 | "slices" 11 | ) 12 | 13 | func UpdatePoolInfo(isFull bool) (messageList []string, err error) { 14 | logInfo, err := util.GetLogInfo() 15 | if err != nil { 16 | return nil, errors.WithStack(err) 17 | } 18 | 19 | messageList = append(messageList, logInfo.Uid) 20 | for _, poolTypeUnit := range preload.PoolTypeMap { 21 | if isFull { 22 | n, err := fullUpdatePoolInfo(logInfo, poolTypeUnit.Id) 23 | if err != nil { 24 | return nil, errors.WithStack(err) 25 | } 26 | messageList = append(messageList, fmt.Sprintf("%s 全量更新%d条数据", poolTypeUnit.Name, n)) 27 | } else { 28 | n, err := incrementalUpdatePoolInfo(logInfo, poolTypeUnit.Id) 29 | if err != nil { 30 | return nil, errors.WithStack(err) 31 | } 32 | messageList = append(messageList, fmt.Sprintf("%s 增量更新%d条数据", poolTypeUnit.Name, n)) 33 | } 34 | } 35 | 36 | return messageList, nil 37 | } 38 | 39 | // incrementalUpdatePoolInfo 增量更新 40 | func incrementalUpdatePoolInfo(logInfo model.LogInfo, poolType int64) (int, error) { 41 | localRecordList, err := GetLocalRecord(logInfo.Uid, poolType, 0) 42 | if err != nil { 43 | return 0, errors.WithStack(err) 44 | } 45 | 46 | var lastLocalRecord model.LocalRecord 47 | 48 | if len(localRecordList) > 0 { 49 | lastLocalRecord = localRecordList[len(localRecordList)-1] 50 | } 51 | 52 | var diffRemoteRecordList []model.RemoteRecord 53 | respData, err := FetchRemoteData(logInfo.GachaUrl, logInfo.AccessToken, "", poolType) 54 | if err != nil { 55 | return 0, errors.WithStack(err) 56 | } 57 | 58 | //todo 只对比一条是有可能有问题的 59 | var flag bool 60 | for i, remoteRecord := range respData.RecordList { 61 | if remoteRecord.ItemId == lastLocalRecord.ItemId && remoteRecord.GachaTimestamp == lastLocalRecord.GachaTimestamp { 62 | flag = true 63 | break 64 | } else { 65 | diffRemoteRecordList = append(diffRemoteRecordList, respData.RecordList[i]) 66 | } 67 | } 68 | for respData.Next != "" && !flag { 69 | //time.Sleep(50 * time.Millisecond) //这个接口似乎没有限制频率 70 | respData, err = FetchRemoteData(logInfo.GachaUrl, logInfo.AccessToken, respData.Next, poolType) 71 | if err != nil { 72 | return 0, errors.WithStack(err) 73 | } 74 | for i, remoteRecord := range respData.RecordList { 75 | if remoteRecord.ItemId == lastLocalRecord.ItemId && remoteRecord.GachaTimestamp == lastLocalRecord.GachaTimestamp { 76 | flag = true 77 | break 78 | } else { 79 | diffRemoteRecordList = append(diffRemoteRecordList, respData.RecordList[i]) 80 | } 81 | } 82 | } 83 | 84 | if len(diffRemoteRecordList) > 0 { 85 | var diffLocalRecordList []model.LocalRecord 86 | for i := len(diffRemoteRecordList) - 1; i >= 0; i-- { 87 | diffLocalRecordList = append(diffLocalRecordList, model.LocalRecord{ 88 | PoolType: poolType, 89 | PoolId: diffRemoteRecordList[i].PoolId, 90 | ItemId: diffRemoteRecordList[i].ItemId, 91 | GachaTimestamp: diffRemoteRecordList[i].GachaTimestamp, 92 | }) 93 | } 94 | err = SaveLocalRecord(logInfo.Uid, diffLocalRecordList) 95 | if err != nil { 96 | return 0, errors.WithStack(err) 97 | } 98 | } 99 | 100 | updateNum := len(diffRemoteRecordList) 101 | logger.Logger.Infof("UID:%s poolType:%d 增量更新%d条数据", logInfo.Uid, poolType, updateNum) 102 | 103 | return updateNum, nil 104 | } 105 | 106 | // fullUpdatePoolInfo 全量更新 107 | func fullUpdatePoolInfo(logInfo model.LogInfo, poolType int64) (int, error) { 108 | var remoteRecordList []model.LocalRecord 109 | 110 | respData, err := FetchRemoteData(logInfo.GachaUrl, logInfo.AccessToken, "", poolType) 111 | if err != nil { 112 | return 0, errors.WithStack(err) 113 | } 114 | for _, record := range respData.RecordList { 115 | remoteRecordList = append(remoteRecordList, model.LocalRecord{ 116 | PoolType: poolType, 117 | PoolId: record.PoolId, 118 | ItemId: record.ItemId, 119 | GachaTimestamp: record.GachaTimestamp, 120 | }) 121 | } 122 | 123 | for respData.Next != "" { 124 | //time.Sleep(50 * time.Millisecond) //这个接口似乎没有限制频率 125 | respData, err = FetchRemoteData(logInfo.GachaUrl, logInfo.AccessToken, respData.Next, poolType) 126 | if err != nil { 127 | return 0, errors.WithStack(err) 128 | } 129 | for _, record := range respData.RecordList { 130 | remoteRecordList = append(remoteRecordList, model.LocalRecord{ 131 | PoolType: poolType, 132 | PoolId: record.PoolId, 133 | ItemId: record.ItemId, 134 | GachaTimestamp: record.GachaTimestamp, 135 | }) 136 | } 137 | } 138 | 139 | if len(remoteRecordList) == 0 { 140 | return 0, nil 141 | } 142 | //合并前先备份 143 | err = util.BackupDB() 144 | if err != nil { 145 | return 0, errors.WithStack(err) 146 | } 147 | 148 | slices.Reverse(remoteRecordList) 149 | 150 | localRecordList, err := GetLocalRecord(logInfo.Uid, poolType, remoteRecordList[0].GachaTimestamp) 151 | if err != nil { 152 | return 0, errors.WithStack(err) 153 | } 154 | 155 | newRecordList := MergeRecord(remoteRecordList, localRecordList) 156 | 157 | err = RemoveLocalRecord(logInfo.Uid, poolType) 158 | if err != nil { 159 | return 0, errors.WithStack(err) 160 | } 161 | 162 | err = SaveLocalRecord(logInfo.Uid, newRecordList) 163 | if err != nil { 164 | return 0, errors.WithStack(err) 165 | } 166 | 167 | updateNum := len(newRecordList) 168 | logger.Logger.Infof("UID:%s poolType:%d 全量更新%d条数据", logInfo.Uid, poolType, updateNum) 169 | 170 | return updateNum, nil 171 | } 172 | -------------------------------------------------------------------------------- /logic/UpdateTo.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "fmt" 5 | "github.com/inconshreveable/go-update" 6 | "github.com/pkg/errors" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | func UpdateTo(version string) error { 13 | resp, err := http.Get(fmt.Sprintf("https://gfl2bucket.mcc.wiki/gf2gacha/%s/gf2gacha.exe", version)) 14 | if err != nil { 15 | return err 16 | } 17 | defer resp.Body.Close() 18 | if resp.StatusCode != http.StatusOK { 19 | return errors.New(resp.Status) 20 | } 21 | 22 | err = update.Apply(resp.Body, update.Options{}) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | err = restart() 28 | if err != nil { 29 | return err 30 | } 31 | os.Exit(0) 32 | 33 | return nil 34 | } 35 | 36 | func restart() error { 37 | execPath, err := os.Executable() 38 | if err != nil { 39 | return err 40 | } 41 | cmd := exec.Command(execPath) 42 | return cmd.Start() 43 | } 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "gf2gacha/logger" 7 | _ "gf2gacha/logger" 8 | "gf2gacha/util" 9 | "github.com/wailsapp/wails/v2" 10 | "github.com/wailsapp/wails/v2/pkg/options" 11 | "github.com/wailsapp/wails/v2/pkg/options/assetserver" 12 | ) 13 | 14 | //go:embed all:frontend/dist 15 | var assets embed.FS 16 | 17 | func main() { 18 | defer logger.Logger.Sync() 19 | defer util.DisableSysProxy() 20 | // Create an instance of the app structure 21 | app := NewApp() 22 | 23 | // Create application with options 24 | err := wails.Run(&options.App{ 25 | Title: fmt.Sprintf("少女前线2:追放 抽卡导出分析工具%v Powered by MccWiki抹茶", util.GetVersion()), 26 | Width: 1280, 27 | Height: 720, 28 | MinWidth: 1280, 29 | MinHeight: 720, 30 | AssetServer: &assetserver.Options{ 31 | Assets: assets, 32 | }, 33 | OnStartup: app.startup, 34 | Bind: []interface{}{ 35 | app, 36 | }, 37 | }) 38 | 39 | if err != nil { 40 | logger.Logger.Error(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /model/community.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type CommunityExchangeList struct { 4 | Id int64 `json:"id"` 5 | Name string `json:"name"` 6 | } 7 | -------------------------------------------------------------------------------- /model/init.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gf2gacha/logger" 5 | _ "modernc.org/sqlite" 6 | "xorm.io/xorm" 7 | ) 8 | 9 | var Engine *xorm.Engine 10 | 11 | func init() { 12 | var err error 13 | Engine, err = xorm.NewEngine("sqlite", "./gf2gacha.db?_pragma=busy_timeout(2000)") 14 | if err != nil { 15 | logger.Logger.Panic(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /model/logInfo.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type LogInfo struct { 4 | TablePath string `json:"tablePath"` 5 | AccessToken string `json:"accessToken"` 6 | Uid string `json:"uid"` 7 | GachaUrl string `json:"gachaUrl"` 8 | } 9 | -------------------------------------------------------------------------------- /model/pool.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Pool struct { 4 | PoolType int64 `json:"poolType"` 5 | GachaCount int64 `json:"gachaCount"` 6 | LoseCount int64 `json:"loseCount"` 7 | GuaranteesCount int64 `json:"guaranteesCount"` 8 | Rank5Count int64 `json:"rank5Count"` 9 | Rank4Count int64 `json:"rank4Count"` 10 | Rank3Count int64 `json:"rank3Count"` 11 | StoredCount int64 `json:"storedCount"` 12 | RecordList []DisplayRecord `json:"recordList"` 13 | } 14 | -------------------------------------------------------------------------------- /model/record.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | type RemoteRecord struct { 6 | PoolId int64 `json:"pool_id"` 7 | ItemId int64 `json:"item"` 8 | GachaTimestamp int64 `json:"time"` 9 | } 10 | 11 | type LocalRecord struct { 12 | Id int64 13 | InsertTime time.Time `xorm:"created"` 14 | PoolType int64 15 | PoolId int64 16 | ItemId int64 17 | GachaTimestamp int64 18 | } 19 | 20 | type DisplayRecord struct { 21 | Id int64 `json:"id"` 22 | Name string `json:"name"` 23 | Lose bool `json:"lose"` 24 | Count int64 `json:"count"` 25 | } 26 | -------------------------------------------------------------------------------- /model/request.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "encoding/json" 4 | 5 | type ResponseBody struct { 6 | Code int `json:"code"` 7 | Message string `json:"message"` 8 | Data json.RawMessage `json:"data"` 9 | } 10 | 11 | type ResponseData struct { 12 | RecordList []RemoteRecord `json:"list"` 13 | Next string `json:"next"` 14 | } 15 | -------------------------------------------------------------------------------- /pb/GachaData.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.31.0 4 | // protoc v4.25.1 5 | // source: proto/GachaData.proto 6 | 7 | package pb 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type GachaData struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Units []*GachaDataUnit `protobuf:"bytes,1,rep,name=Units,proto3" json:"Units,omitempty"` 29 | } 30 | 31 | func (x *GachaData) Reset() { 32 | *x = GachaData{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_proto_GachaData_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *GachaData) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*GachaData) ProtoMessage() {} 45 | 46 | func (x *GachaData) ProtoReflect() protoreflect.Message { 47 | mi := &file_proto_GachaData_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use GachaData.ProtoReflect.Descriptor instead. 59 | func (*GachaData) Descriptor() ([]byte, []int) { 60 | return file_proto_GachaData_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *GachaData) GetUnits() []*GachaDataUnit { 64 | if x != nil { 65 | return x.Units 66 | } 67 | return nil 68 | } 69 | 70 | type GachaDataUnit struct { 71 | state protoimpl.MessageState 72 | sizeCache protoimpl.SizeCache 73 | unknownFields protoimpl.UnknownFields 74 | 75 | Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` 76 | Type int64 `protobuf:"varint,4,opt,name=type,proto3" json:"type,omitempty"` 77 | StartTime int64 `protobuf:"varint,7,opt,name=startTime,proto3" json:"startTime,omitempty"` 78 | EndTime int64 `protobuf:"varint,8,opt,name=endTime,proto3" json:"endTime,omitempty"` 79 | Name *LanguageStringData `protobuf:"bytes,9,opt,name=name,proto3" json:"name,omitempty"` 80 | RateDesGun string `protobuf:"bytes,21,opt,name=rateDesGun,proto3" json:"rateDesGun,omitempty"` 81 | RateDesWeapon string `protobuf:"bytes,22,opt,name=rateDesWeapon,proto3" json:"rateDesWeapon,omitempty"` 82 | GunUpItem string `protobuf:"bytes,26,opt,name=gunUpItem,proto3" json:"gunUpItem,omitempty"` 83 | WeaponUpItem string `protobuf:"bytes,28,opt,name=weaponUpItem,proto3" json:"weaponUpItem,omitempty"` 84 | } 85 | 86 | func (x *GachaDataUnit) Reset() { 87 | *x = GachaDataUnit{} 88 | if protoimpl.UnsafeEnabled { 89 | mi := &file_proto_GachaData_proto_msgTypes[1] 90 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 91 | ms.StoreMessageInfo(mi) 92 | } 93 | } 94 | 95 | func (x *GachaDataUnit) String() string { 96 | return protoimpl.X.MessageStringOf(x) 97 | } 98 | 99 | func (*GachaDataUnit) ProtoMessage() {} 100 | 101 | func (x *GachaDataUnit) ProtoReflect() protoreflect.Message { 102 | mi := &file_proto_GachaData_proto_msgTypes[1] 103 | if protoimpl.UnsafeEnabled && x != nil { 104 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 105 | if ms.LoadMessageInfo() == nil { 106 | ms.StoreMessageInfo(mi) 107 | } 108 | return ms 109 | } 110 | return mi.MessageOf(x) 111 | } 112 | 113 | // Deprecated: Use GachaDataUnit.ProtoReflect.Descriptor instead. 114 | func (*GachaDataUnit) Descriptor() ([]byte, []int) { 115 | return file_proto_GachaData_proto_rawDescGZIP(), []int{1} 116 | } 117 | 118 | func (x *GachaDataUnit) GetId() int64 { 119 | if x != nil { 120 | return x.Id 121 | } 122 | return 0 123 | } 124 | 125 | func (x *GachaDataUnit) GetType() int64 { 126 | if x != nil { 127 | return x.Type 128 | } 129 | return 0 130 | } 131 | 132 | func (x *GachaDataUnit) GetStartTime() int64 { 133 | if x != nil { 134 | return x.StartTime 135 | } 136 | return 0 137 | } 138 | 139 | func (x *GachaDataUnit) GetEndTime() int64 { 140 | if x != nil { 141 | return x.EndTime 142 | } 143 | return 0 144 | } 145 | 146 | func (x *GachaDataUnit) GetName() *LanguageStringData { 147 | if x != nil { 148 | return x.Name 149 | } 150 | return nil 151 | } 152 | 153 | func (x *GachaDataUnit) GetRateDesGun() string { 154 | if x != nil { 155 | return x.RateDesGun 156 | } 157 | return "" 158 | } 159 | 160 | func (x *GachaDataUnit) GetRateDesWeapon() string { 161 | if x != nil { 162 | return x.RateDesWeapon 163 | } 164 | return "" 165 | } 166 | 167 | func (x *GachaDataUnit) GetGunUpItem() string { 168 | if x != nil { 169 | return x.GunUpItem 170 | } 171 | return "" 172 | } 173 | 174 | func (x *GachaDataUnit) GetWeaponUpItem() string { 175 | if x != nil { 176 | return x.WeaponUpItem 177 | } 178 | return "" 179 | } 180 | 181 | var File_proto_GachaData_proto protoreflect.FileDescriptor 182 | 183 | var file_proto_GachaData_proto_rawDesc = []byte{ 184 | 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x47, 0x61, 0x63, 0x68, 0x61, 0x44, 0x61, 0x74, 185 | 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x4c, 186 | 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x44, 0x61, 0x74, 187 | 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x31, 0x0a, 0x09, 0x47, 0x61, 0x63, 0x68, 0x61, 188 | 0x44, 0x61, 0x74, 0x61, 0x12, 0x24, 0x0a, 0x05, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 189 | 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x47, 0x61, 0x63, 0x68, 0x61, 0x44, 0x61, 0x74, 0x61, 0x55, 190 | 0x6e, 0x69, 0x74, 0x52, 0x05, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x22, 0x9c, 0x02, 0x0a, 0x0d, 0x47, 191 | 0x61, 0x63, 0x68, 0x61, 0x44, 0x61, 0x74, 0x61, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x0e, 0x0a, 0x02, 192 | 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 193 | 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 194 | 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 195 | 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 196 | 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 197 | 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 198 | 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 199 | 0x65, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x6e, 0x61, 0x6d, 200 | 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x61, 0x74, 0x65, 0x44, 0x65, 0x73, 0x47, 0x75, 0x6e, 0x18, 201 | 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x61, 0x74, 0x65, 0x44, 0x65, 0x73, 0x47, 0x75, 202 | 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x61, 0x74, 0x65, 0x44, 0x65, 0x73, 0x57, 0x65, 0x61, 0x70, 203 | 0x6f, 0x6e, 0x18, 0x16, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x61, 0x74, 0x65, 0x44, 0x65, 204 | 0x73, 0x57, 0x65, 0x61, 0x70, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x67, 0x75, 0x6e, 0x55, 0x70, 205 | 0x49, 0x74, 0x65, 0x6d, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x75, 0x6e, 0x55, 206 | 0x70, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x0c, 0x77, 0x65, 0x61, 0x70, 0x6f, 0x6e, 0x55, 207 | 0x70, 0x49, 0x74, 0x65, 0x6d, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x77, 0x65, 0x61, 208 | 0x70, 0x6f, 0x6e, 0x55, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 209 | 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 210 | } 211 | 212 | var ( 213 | file_proto_GachaData_proto_rawDescOnce sync.Once 214 | file_proto_GachaData_proto_rawDescData = file_proto_GachaData_proto_rawDesc 215 | ) 216 | 217 | func file_proto_GachaData_proto_rawDescGZIP() []byte { 218 | file_proto_GachaData_proto_rawDescOnce.Do(func() { 219 | file_proto_GachaData_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_GachaData_proto_rawDescData) 220 | }) 221 | return file_proto_GachaData_proto_rawDescData 222 | } 223 | 224 | var file_proto_GachaData_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 225 | var file_proto_GachaData_proto_goTypes = []interface{}{ 226 | (*GachaData)(nil), // 0: GachaData 227 | (*GachaDataUnit)(nil), // 1: GachaDataUnit 228 | (*LanguageStringData)(nil), // 2: LanguageStringData 229 | } 230 | var file_proto_GachaData_proto_depIdxs = []int32{ 231 | 1, // 0: GachaData.Units:type_name -> GachaDataUnit 232 | 2, // 1: GachaDataUnit.name:type_name -> LanguageStringData 233 | 2, // [2:2] is the sub-list for method output_type 234 | 2, // [2:2] is the sub-list for method input_type 235 | 2, // [2:2] is the sub-list for extension type_name 236 | 2, // [2:2] is the sub-list for extension extendee 237 | 0, // [0:2] is the sub-list for field type_name 238 | } 239 | 240 | func init() { file_proto_GachaData_proto_init() } 241 | func file_proto_GachaData_proto_init() { 242 | if File_proto_GachaData_proto != nil { 243 | return 244 | } 245 | file_proto_LanguageStringData_proto_init() 246 | if !protoimpl.UnsafeEnabled { 247 | file_proto_GachaData_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 248 | switch v := v.(*GachaData); i { 249 | case 0: 250 | return &v.state 251 | case 1: 252 | return &v.sizeCache 253 | case 2: 254 | return &v.unknownFields 255 | default: 256 | return nil 257 | } 258 | } 259 | file_proto_GachaData_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 260 | switch v := v.(*GachaDataUnit); i { 261 | case 0: 262 | return &v.state 263 | case 1: 264 | return &v.sizeCache 265 | case 2: 266 | return &v.unknownFields 267 | default: 268 | return nil 269 | } 270 | } 271 | } 272 | type x struct{} 273 | out := protoimpl.TypeBuilder{ 274 | File: protoimpl.DescBuilder{ 275 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 276 | RawDescriptor: file_proto_GachaData_proto_rawDesc, 277 | NumEnums: 0, 278 | NumMessages: 2, 279 | NumExtensions: 0, 280 | NumServices: 0, 281 | }, 282 | GoTypes: file_proto_GachaData_proto_goTypes, 283 | DependencyIndexes: file_proto_GachaData_proto_depIdxs, 284 | MessageInfos: file_proto_GachaData_proto_msgTypes, 285 | }.Build() 286 | File_proto_GachaData_proto = out.File 287 | file_proto_GachaData_proto_rawDesc = nil 288 | file_proto_GachaData_proto_goTypes = nil 289 | file_proto_GachaData_proto_depIdxs = nil 290 | } 291 | -------------------------------------------------------------------------------- /pb/GachaTypeListData.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.31.0 4 | // protoc v4.25.1 5 | // source: proto/GachaTypeListData.proto 6 | 7 | package pb 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type GachaTypeListData struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Units []*GachaTypeListDataUnit `protobuf:"bytes,1,rep,name=Units,proto3" json:"Units,omitempty"` 29 | } 30 | 31 | func (x *GachaTypeListData) Reset() { 32 | *x = GachaTypeListData{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_proto_GachaTypeListData_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *GachaTypeListData) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*GachaTypeListData) ProtoMessage() {} 45 | 46 | func (x *GachaTypeListData) ProtoReflect() protoreflect.Message { 47 | mi := &file_proto_GachaTypeListData_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use GachaTypeListData.ProtoReflect.Descriptor instead. 59 | func (*GachaTypeListData) Descriptor() ([]byte, []int) { 60 | return file_proto_GachaTypeListData_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *GachaTypeListData) GetUnits() []*GachaTypeListDataUnit { 64 | if x != nil { 65 | return x.Units 66 | } 67 | return nil 68 | } 69 | 70 | type GachaTypeListDataUnit struct { 71 | state protoimpl.MessageState 72 | sizeCache protoimpl.SizeCache 73 | unknownFields protoimpl.UnknownFields 74 | 75 | Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` 76 | Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 77 | } 78 | 79 | func (x *GachaTypeListDataUnit) Reset() { 80 | *x = GachaTypeListDataUnit{} 81 | if protoimpl.UnsafeEnabled { 82 | mi := &file_proto_GachaTypeListData_proto_msgTypes[1] 83 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 84 | ms.StoreMessageInfo(mi) 85 | } 86 | } 87 | 88 | func (x *GachaTypeListDataUnit) String() string { 89 | return protoimpl.X.MessageStringOf(x) 90 | } 91 | 92 | func (*GachaTypeListDataUnit) ProtoMessage() {} 93 | 94 | func (x *GachaTypeListDataUnit) ProtoReflect() protoreflect.Message { 95 | mi := &file_proto_GachaTypeListData_proto_msgTypes[1] 96 | if protoimpl.UnsafeEnabled && x != nil { 97 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 98 | if ms.LoadMessageInfo() == nil { 99 | ms.StoreMessageInfo(mi) 100 | } 101 | return ms 102 | } 103 | return mi.MessageOf(x) 104 | } 105 | 106 | // Deprecated: Use GachaTypeListDataUnit.ProtoReflect.Descriptor instead. 107 | func (*GachaTypeListDataUnit) Descriptor() ([]byte, []int) { 108 | return file_proto_GachaTypeListData_proto_rawDescGZIP(), []int{1} 109 | } 110 | 111 | func (x *GachaTypeListDataUnit) GetId() int64 { 112 | if x != nil { 113 | return x.Id 114 | } 115 | return 0 116 | } 117 | 118 | func (x *GachaTypeListDataUnit) GetName() string { 119 | if x != nil { 120 | return x.Name 121 | } 122 | return "" 123 | } 124 | 125 | var File_proto_GachaTypeListData_proto protoreflect.FileDescriptor 126 | 127 | var file_proto_GachaTypeListData_proto_rawDesc = []byte{ 128 | 0x0a, 0x1d, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x47, 0x61, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 129 | 0x65, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 130 | 0x41, 0x0a, 0x11, 0x47, 0x61, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x4c, 0x69, 0x73, 0x74, 131 | 0x44, 0x61, 0x74, 0x61, 0x12, 0x2c, 0x0a, 0x05, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 132 | 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x47, 0x61, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x4c, 133 | 0x69, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x55, 0x6e, 0x69, 0x74, 0x52, 0x05, 0x55, 0x6e, 0x69, 134 | 0x74, 0x73, 0x22, 0x3b, 0x0a, 0x15, 0x47, 0x61, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x4c, 135 | 0x69, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 136 | 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 137 | 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, 138 | 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 139 | 0x6f, 0x33, 140 | } 141 | 142 | var ( 143 | file_proto_GachaTypeListData_proto_rawDescOnce sync.Once 144 | file_proto_GachaTypeListData_proto_rawDescData = file_proto_GachaTypeListData_proto_rawDesc 145 | ) 146 | 147 | func file_proto_GachaTypeListData_proto_rawDescGZIP() []byte { 148 | file_proto_GachaTypeListData_proto_rawDescOnce.Do(func() { 149 | file_proto_GachaTypeListData_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_GachaTypeListData_proto_rawDescData) 150 | }) 151 | return file_proto_GachaTypeListData_proto_rawDescData 152 | } 153 | 154 | var file_proto_GachaTypeListData_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 155 | var file_proto_GachaTypeListData_proto_goTypes = []interface{}{ 156 | (*GachaTypeListData)(nil), // 0: GachaTypeListData 157 | (*GachaTypeListDataUnit)(nil), // 1: GachaTypeListDataUnit 158 | } 159 | var file_proto_GachaTypeListData_proto_depIdxs = []int32{ 160 | 1, // 0: GachaTypeListData.Units:type_name -> GachaTypeListDataUnit 161 | 1, // [1:1] is the sub-list for method output_type 162 | 1, // [1:1] is the sub-list for method input_type 163 | 1, // [1:1] is the sub-list for extension type_name 164 | 1, // [1:1] is the sub-list for extension extendee 165 | 0, // [0:1] is the sub-list for field type_name 166 | } 167 | 168 | func init() { file_proto_GachaTypeListData_proto_init() } 169 | func file_proto_GachaTypeListData_proto_init() { 170 | if File_proto_GachaTypeListData_proto != nil { 171 | return 172 | } 173 | if !protoimpl.UnsafeEnabled { 174 | file_proto_GachaTypeListData_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 175 | switch v := v.(*GachaTypeListData); i { 176 | case 0: 177 | return &v.state 178 | case 1: 179 | return &v.sizeCache 180 | case 2: 181 | return &v.unknownFields 182 | default: 183 | return nil 184 | } 185 | } 186 | file_proto_GachaTypeListData_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 187 | switch v := v.(*GachaTypeListDataUnit); i { 188 | case 0: 189 | return &v.state 190 | case 1: 191 | return &v.sizeCache 192 | case 2: 193 | return &v.unknownFields 194 | default: 195 | return nil 196 | } 197 | } 198 | } 199 | type x struct{} 200 | out := protoimpl.TypeBuilder{ 201 | File: protoimpl.DescBuilder{ 202 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 203 | RawDescriptor: file_proto_GachaTypeListData_proto_rawDesc, 204 | NumEnums: 0, 205 | NumMessages: 2, 206 | NumExtensions: 0, 207 | NumServices: 0, 208 | }, 209 | GoTypes: file_proto_GachaTypeListData_proto_goTypes, 210 | DependencyIndexes: file_proto_GachaTypeListData_proto_depIdxs, 211 | MessageInfos: file_proto_GachaTypeListData_proto_msgTypes, 212 | }.Build() 213 | File_proto_GachaTypeListData_proto = out.File 214 | file_proto_GachaTypeListData_proto_rawDesc = nil 215 | file_proto_GachaTypeListData_proto_goTypes = nil 216 | file_proto_GachaTypeListData_proto_depIdxs = nil 217 | } 218 | -------------------------------------------------------------------------------- /pb/ItemData.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.31.0 4 | // protoc v4.25.1 5 | // source: proto/ItemData.proto 6 | 7 | package pb 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type ItemData struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Units []*ItemDataUnit `protobuf:"bytes,1,rep,name=Units,proto3" json:"Units,omitempty"` 29 | } 30 | 31 | func (x *ItemData) Reset() { 32 | *x = ItemData{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_proto_ItemData_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *ItemData) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*ItemData) ProtoMessage() {} 45 | 46 | func (x *ItemData) ProtoReflect() protoreflect.Message { 47 | mi := &file_proto_ItemData_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use ItemData.ProtoReflect.Descriptor instead. 59 | func (*ItemData) Descriptor() ([]byte, []int) { 60 | return file_proto_ItemData_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *ItemData) GetUnits() []*ItemDataUnit { 64 | if x != nil { 65 | return x.Units 66 | } 67 | return nil 68 | } 69 | 70 | type ItemDataUnit struct { 71 | state protoimpl.MessageState 72 | sizeCache protoimpl.SizeCache 73 | unknownFields protoimpl.UnknownFields 74 | 75 | Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` 76 | Name *LanguageStringData `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 77 | Type int64 `protobuf:"varint,5,opt,name=type,proto3" json:"type,omitempty"` 78 | Icon string `protobuf:"bytes,6,opt,name=icon,proto3" json:"icon,omitempty"` 79 | Rank int64 `protobuf:"varint,7,opt,name=rank,proto3" json:"rank,omitempty"` 80 | } 81 | 82 | func (x *ItemDataUnit) Reset() { 83 | *x = ItemDataUnit{} 84 | if protoimpl.UnsafeEnabled { 85 | mi := &file_proto_ItemData_proto_msgTypes[1] 86 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 87 | ms.StoreMessageInfo(mi) 88 | } 89 | } 90 | 91 | func (x *ItemDataUnit) String() string { 92 | return protoimpl.X.MessageStringOf(x) 93 | } 94 | 95 | func (*ItemDataUnit) ProtoMessage() {} 96 | 97 | func (x *ItemDataUnit) ProtoReflect() protoreflect.Message { 98 | mi := &file_proto_ItemData_proto_msgTypes[1] 99 | if protoimpl.UnsafeEnabled && x != nil { 100 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 101 | if ms.LoadMessageInfo() == nil { 102 | ms.StoreMessageInfo(mi) 103 | } 104 | return ms 105 | } 106 | return mi.MessageOf(x) 107 | } 108 | 109 | // Deprecated: Use ItemDataUnit.ProtoReflect.Descriptor instead. 110 | func (*ItemDataUnit) Descriptor() ([]byte, []int) { 111 | return file_proto_ItemData_proto_rawDescGZIP(), []int{1} 112 | } 113 | 114 | func (x *ItemDataUnit) GetId() int64 { 115 | if x != nil { 116 | return x.Id 117 | } 118 | return 0 119 | } 120 | 121 | func (x *ItemDataUnit) GetName() *LanguageStringData { 122 | if x != nil { 123 | return x.Name 124 | } 125 | return nil 126 | } 127 | 128 | func (x *ItemDataUnit) GetType() int64 { 129 | if x != nil { 130 | return x.Type 131 | } 132 | return 0 133 | } 134 | 135 | func (x *ItemDataUnit) GetIcon() string { 136 | if x != nil { 137 | return x.Icon 138 | } 139 | return "" 140 | } 141 | 142 | func (x *ItemDataUnit) GetRank() int64 { 143 | if x != nil { 144 | return x.Rank 145 | } 146 | return 0 147 | } 148 | 149 | var File_proto_ItemData_proto protoreflect.FileDescriptor 150 | 151 | var file_proto_ItemData_proto_rawDesc = []byte{ 152 | 0x0a, 0x14, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x49, 0x74, 0x65, 0x6d, 0x44, 0x61, 0x74, 0x61, 153 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x4c, 0x61, 154 | 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x44, 0x61, 0x74, 0x61, 155 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2f, 0x0a, 0x08, 0x49, 0x74, 0x65, 0x6d, 0x44, 0x61, 156 | 0x74, 0x61, 0x12, 0x23, 0x0a, 0x05, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 157 | 0x0b, 0x32, 0x0d, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x55, 0x6e, 0x69, 0x74, 158 | 0x52, 0x05, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x22, 0x83, 0x01, 0x0a, 0x0c, 0x49, 0x74, 0x65, 0x6d, 159 | 0x44, 0x61, 0x74, 0x61, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 160 | 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x27, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 161 | 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 162 | 0x65, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x6e, 0x61, 0x6d, 163 | 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 164 | 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 165 | 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x61, 0x6e, 166 | 0x6b, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x72, 0x61, 0x6e, 0x6b, 0x42, 0x09, 0x5a, 167 | 0x07, 0x2e, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 168 | } 169 | 170 | var ( 171 | file_proto_ItemData_proto_rawDescOnce sync.Once 172 | file_proto_ItemData_proto_rawDescData = file_proto_ItemData_proto_rawDesc 173 | ) 174 | 175 | func file_proto_ItemData_proto_rawDescGZIP() []byte { 176 | file_proto_ItemData_proto_rawDescOnce.Do(func() { 177 | file_proto_ItemData_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_ItemData_proto_rawDescData) 178 | }) 179 | return file_proto_ItemData_proto_rawDescData 180 | } 181 | 182 | var file_proto_ItemData_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 183 | var file_proto_ItemData_proto_goTypes = []interface{}{ 184 | (*ItemData)(nil), // 0: ItemData 185 | (*ItemDataUnit)(nil), // 1: ItemDataUnit 186 | (*LanguageStringData)(nil), // 2: LanguageStringData 187 | } 188 | var file_proto_ItemData_proto_depIdxs = []int32{ 189 | 1, // 0: ItemData.Units:type_name -> ItemDataUnit 190 | 2, // 1: ItemDataUnit.name:type_name -> LanguageStringData 191 | 2, // [2:2] is the sub-list for method output_type 192 | 2, // [2:2] is the sub-list for method input_type 193 | 2, // [2:2] is the sub-list for extension type_name 194 | 2, // [2:2] is the sub-list for extension extendee 195 | 0, // [0:2] is the sub-list for field type_name 196 | } 197 | 198 | func init() { file_proto_ItemData_proto_init() } 199 | func file_proto_ItemData_proto_init() { 200 | if File_proto_ItemData_proto != nil { 201 | return 202 | } 203 | file_proto_LanguageStringData_proto_init() 204 | if !protoimpl.UnsafeEnabled { 205 | file_proto_ItemData_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 206 | switch v := v.(*ItemData); i { 207 | case 0: 208 | return &v.state 209 | case 1: 210 | return &v.sizeCache 211 | case 2: 212 | return &v.unknownFields 213 | default: 214 | return nil 215 | } 216 | } 217 | file_proto_ItemData_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 218 | switch v := v.(*ItemDataUnit); i { 219 | case 0: 220 | return &v.state 221 | case 1: 222 | return &v.sizeCache 223 | case 2: 224 | return &v.unknownFields 225 | default: 226 | return nil 227 | } 228 | } 229 | } 230 | type x struct{} 231 | out := protoimpl.TypeBuilder{ 232 | File: protoimpl.DescBuilder{ 233 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 234 | RawDescriptor: file_proto_ItemData_proto_rawDesc, 235 | NumEnums: 0, 236 | NumMessages: 2, 237 | NumExtensions: 0, 238 | NumServices: 0, 239 | }, 240 | GoTypes: file_proto_ItemData_proto_goTypes, 241 | DependencyIndexes: file_proto_ItemData_proto_depIdxs, 242 | MessageInfos: file_proto_ItemData_proto_msgTypes, 243 | }.Build() 244 | File_proto_ItemData_proto = out.File 245 | file_proto_ItemData_proto_rawDesc = nil 246 | file_proto_ItemData_proto_goTypes = nil 247 | file_proto_ItemData_proto_depIdxs = nil 248 | } 249 | -------------------------------------------------------------------------------- /pb/LangPackageTableCnData.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.31.0 4 | // protoc v4.25.1 5 | // source: proto/LangPackageTableCnData.proto 6 | 7 | package pb 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type LangPackageTableCnData struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Units []*LangPackageTableCnDataUnit `protobuf:"bytes,1,rep,name=Units,proto3" json:"Units,omitempty"` 29 | } 30 | 31 | func (x *LangPackageTableCnData) Reset() { 32 | *x = LangPackageTableCnData{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_proto_LangPackageTableCnData_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *LangPackageTableCnData) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*LangPackageTableCnData) ProtoMessage() {} 45 | 46 | func (x *LangPackageTableCnData) ProtoReflect() protoreflect.Message { 47 | mi := &file_proto_LangPackageTableCnData_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use LangPackageTableCnData.ProtoReflect.Descriptor instead. 59 | func (*LangPackageTableCnData) Descriptor() ([]byte, []int) { 60 | return file_proto_LangPackageTableCnData_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *LangPackageTableCnData) GetUnits() []*LangPackageTableCnDataUnit { 64 | if x != nil { 65 | return x.Units 66 | } 67 | return nil 68 | } 69 | 70 | type LangPackageTableCnDataUnit struct { 71 | state protoimpl.MessageState 72 | sizeCache protoimpl.SizeCache 73 | unknownFields protoimpl.UnknownFields 74 | 75 | Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` 76 | Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` 77 | } 78 | 79 | func (x *LangPackageTableCnDataUnit) Reset() { 80 | *x = LangPackageTableCnDataUnit{} 81 | if protoimpl.UnsafeEnabled { 82 | mi := &file_proto_LangPackageTableCnData_proto_msgTypes[1] 83 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 84 | ms.StoreMessageInfo(mi) 85 | } 86 | } 87 | 88 | func (x *LangPackageTableCnDataUnit) String() string { 89 | return protoimpl.X.MessageStringOf(x) 90 | } 91 | 92 | func (*LangPackageTableCnDataUnit) ProtoMessage() {} 93 | 94 | func (x *LangPackageTableCnDataUnit) ProtoReflect() protoreflect.Message { 95 | mi := &file_proto_LangPackageTableCnData_proto_msgTypes[1] 96 | if protoimpl.UnsafeEnabled && x != nil { 97 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 98 | if ms.LoadMessageInfo() == nil { 99 | ms.StoreMessageInfo(mi) 100 | } 101 | return ms 102 | } 103 | return mi.MessageOf(x) 104 | } 105 | 106 | // Deprecated: Use LangPackageTableCnDataUnit.ProtoReflect.Descriptor instead. 107 | func (*LangPackageTableCnDataUnit) Descriptor() ([]byte, []int) { 108 | return file_proto_LangPackageTableCnData_proto_rawDescGZIP(), []int{1} 109 | } 110 | 111 | func (x *LangPackageTableCnDataUnit) GetId() int64 { 112 | if x != nil { 113 | return x.Id 114 | } 115 | return 0 116 | } 117 | 118 | func (x *LangPackageTableCnDataUnit) GetContent() string { 119 | if x != nil { 120 | return x.Content 121 | } 122 | return "" 123 | } 124 | 125 | var File_proto_LangPackageTableCnData_proto protoreflect.FileDescriptor 126 | 127 | var file_proto_LangPackageTableCnData_proto_rawDesc = []byte{ 128 | 0x0a, 0x22, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x4c, 0x61, 0x6e, 0x67, 0x50, 0x61, 0x63, 0x6b, 129 | 0x61, 0x67, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x2e, 0x70, 130 | 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x4b, 0x0a, 0x16, 0x4c, 0x61, 0x6e, 0x67, 0x50, 0x61, 0x63, 0x6b, 131 | 0x61, 0x67, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x31, 132 | 0x0a, 0x05, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 133 | 0x4c, 0x61, 0x6e, 0x67, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 134 | 0x43, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x55, 0x6e, 0x69, 0x74, 0x52, 0x05, 0x55, 0x6e, 0x69, 0x74, 135 | 0x73, 0x22, 0x46, 0x0a, 0x1a, 0x4c, 0x61, 0x6e, 0x67, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 136 | 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x55, 0x6e, 0x69, 0x74, 0x12, 137 | 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 138 | 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 139 | 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 140 | 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 141 | } 142 | 143 | var ( 144 | file_proto_LangPackageTableCnData_proto_rawDescOnce sync.Once 145 | file_proto_LangPackageTableCnData_proto_rawDescData = file_proto_LangPackageTableCnData_proto_rawDesc 146 | ) 147 | 148 | func file_proto_LangPackageTableCnData_proto_rawDescGZIP() []byte { 149 | file_proto_LangPackageTableCnData_proto_rawDescOnce.Do(func() { 150 | file_proto_LangPackageTableCnData_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_LangPackageTableCnData_proto_rawDescData) 151 | }) 152 | return file_proto_LangPackageTableCnData_proto_rawDescData 153 | } 154 | 155 | var file_proto_LangPackageTableCnData_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 156 | var file_proto_LangPackageTableCnData_proto_goTypes = []interface{}{ 157 | (*LangPackageTableCnData)(nil), // 0: LangPackageTableCnData 158 | (*LangPackageTableCnDataUnit)(nil), // 1: LangPackageTableCnDataUnit 159 | } 160 | var file_proto_LangPackageTableCnData_proto_depIdxs = []int32{ 161 | 1, // 0: LangPackageTableCnData.Units:type_name -> LangPackageTableCnDataUnit 162 | 1, // [1:1] is the sub-list for method output_type 163 | 1, // [1:1] is the sub-list for method input_type 164 | 1, // [1:1] is the sub-list for extension type_name 165 | 1, // [1:1] is the sub-list for extension extendee 166 | 0, // [0:1] is the sub-list for field type_name 167 | } 168 | 169 | func init() { file_proto_LangPackageTableCnData_proto_init() } 170 | func file_proto_LangPackageTableCnData_proto_init() { 171 | if File_proto_LangPackageTableCnData_proto != nil { 172 | return 173 | } 174 | if !protoimpl.UnsafeEnabled { 175 | file_proto_LangPackageTableCnData_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 176 | switch v := v.(*LangPackageTableCnData); i { 177 | case 0: 178 | return &v.state 179 | case 1: 180 | return &v.sizeCache 181 | case 2: 182 | return &v.unknownFields 183 | default: 184 | return nil 185 | } 186 | } 187 | file_proto_LangPackageTableCnData_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 188 | switch v := v.(*LangPackageTableCnDataUnit); i { 189 | case 0: 190 | return &v.state 191 | case 1: 192 | return &v.sizeCache 193 | case 2: 194 | return &v.unknownFields 195 | default: 196 | return nil 197 | } 198 | } 199 | } 200 | type x struct{} 201 | out := protoimpl.TypeBuilder{ 202 | File: protoimpl.DescBuilder{ 203 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 204 | RawDescriptor: file_proto_LangPackageTableCnData_proto_rawDesc, 205 | NumEnums: 0, 206 | NumMessages: 2, 207 | NumExtensions: 0, 208 | NumServices: 0, 209 | }, 210 | GoTypes: file_proto_LangPackageTableCnData_proto_goTypes, 211 | DependencyIndexes: file_proto_LangPackageTableCnData_proto_depIdxs, 212 | MessageInfos: file_proto_LangPackageTableCnData_proto_msgTypes, 213 | }.Build() 214 | File_proto_LangPackageTableCnData_proto = out.File 215 | file_proto_LangPackageTableCnData_proto_rawDesc = nil 216 | file_proto_LangPackageTableCnData_proto_goTypes = nil 217 | file_proto_LangPackageTableCnData_proto_depIdxs = nil 218 | } 219 | -------------------------------------------------------------------------------- /pb/LanguageStringData.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.31.0 4 | // protoc v4.25.1 5 | // source: proto/LanguageStringData.proto 6 | 7 | package pb 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type LanguageStringData struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` 29 | } 30 | 31 | func (x *LanguageStringData) Reset() { 32 | *x = LanguageStringData{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_proto_LanguageStringData_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *LanguageStringData) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*LanguageStringData) ProtoMessage() {} 45 | 46 | func (x *LanguageStringData) ProtoReflect() protoreflect.Message { 47 | mi := &file_proto_LanguageStringData_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use LanguageStringData.ProtoReflect.Descriptor instead. 59 | func (*LanguageStringData) Descriptor() ([]byte, []int) { 60 | return file_proto_LanguageStringData_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *LanguageStringData) GetId() int64 { 64 | if x != nil { 65 | return x.Id 66 | } 67 | return 0 68 | } 69 | 70 | var File_proto_LanguageStringData_proto protoreflect.FileDescriptor 71 | 72 | var file_proto_LanguageStringData_proto_rawDesc = []byte{ 73 | 0x0a, 0x1e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 74 | 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x44, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 75 | 0x22, 0x24, 0x0a, 0x12, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x53, 0x74, 0x72, 0x69, 76 | 0x6e, 0x67, 0x44, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 77 | 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x62, 0x3b, 0x70, 78 | 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 79 | } 80 | 81 | var ( 82 | file_proto_LanguageStringData_proto_rawDescOnce sync.Once 83 | file_proto_LanguageStringData_proto_rawDescData = file_proto_LanguageStringData_proto_rawDesc 84 | ) 85 | 86 | func file_proto_LanguageStringData_proto_rawDescGZIP() []byte { 87 | file_proto_LanguageStringData_proto_rawDescOnce.Do(func() { 88 | file_proto_LanguageStringData_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_LanguageStringData_proto_rawDescData) 89 | }) 90 | return file_proto_LanguageStringData_proto_rawDescData 91 | } 92 | 93 | var file_proto_LanguageStringData_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 94 | var file_proto_LanguageStringData_proto_goTypes = []interface{}{ 95 | (*LanguageStringData)(nil), // 0: LanguageStringData 96 | } 97 | var file_proto_LanguageStringData_proto_depIdxs = []int32{ 98 | 0, // [0:0] is the sub-list for method output_type 99 | 0, // [0:0] is the sub-list for method input_type 100 | 0, // [0:0] is the sub-list for extension type_name 101 | 0, // [0:0] is the sub-list for extension extendee 102 | 0, // [0:0] is the sub-list for field type_name 103 | } 104 | 105 | func init() { file_proto_LanguageStringData_proto_init() } 106 | func file_proto_LanguageStringData_proto_init() { 107 | if File_proto_LanguageStringData_proto != nil { 108 | return 109 | } 110 | if !protoimpl.UnsafeEnabled { 111 | file_proto_LanguageStringData_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 112 | switch v := v.(*LanguageStringData); i { 113 | case 0: 114 | return &v.state 115 | case 1: 116 | return &v.sizeCache 117 | case 2: 118 | return &v.unknownFields 119 | default: 120 | return nil 121 | } 122 | } 123 | } 124 | type x struct{} 125 | out := protoimpl.TypeBuilder{ 126 | File: protoimpl.DescBuilder{ 127 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 128 | RawDescriptor: file_proto_LanguageStringData_proto_rawDesc, 129 | NumEnums: 0, 130 | NumMessages: 1, 131 | NumExtensions: 0, 132 | NumServices: 0, 133 | }, 134 | GoTypes: file_proto_LanguageStringData_proto_goTypes, 135 | DependencyIndexes: file_proto_LanguageStringData_proto_depIdxs, 136 | MessageInfos: file_proto_LanguageStringData_proto_msgTypes, 137 | }.Build() 138 | File_proto_LanguageStringData_proto = out.File 139 | file_proto_LanguageStringData_proto_rawDesc = nil 140 | file_proto_LanguageStringData_proto_goTypes = nil 141 | file_proto_LanguageStringData_proto_depIdxs = nil 142 | } 143 | -------------------------------------------------------------------------------- /preload/init.go: -------------------------------------------------------------------------------- 1 | package preload 2 | 3 | import ( 4 | "gf2gacha/logger" 5 | "gf2gacha/pb" 6 | "gf2gacha/util" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | LangMap = make(map[int64]string) 14 | ItemMap = make(map[int64]*pb.ItemDataUnit) 15 | UpItemMap = make(map[int64]int64) 16 | ItemRankMap = make(map[int64]map[int64]int64) 17 | 18 | PoolTypeMap = make(map[int64]*pb.GachaTypeListDataUnit) 19 | 20 | DollNameMapping = make(map[string]int64) 21 | WeaponNameMapping = make(map[string]int64) 22 | ) 23 | 24 | func init() { 25 | //info, err := util.GetLogInfo() 26 | //if err != nil { 27 | // logger.Logger.Panic(err) 28 | //} 29 | 30 | gameDataDir, err := util.GetGameDataDir() 31 | if err != nil { 32 | logger.Logger.Panic(err) 33 | } 34 | tableDir := filepath.Join(gameDataDir, "Table") 35 | 36 | var langCNData pb.LangPackageTableCnData 37 | err = util.GetTableData(tableDir, &langCNData) 38 | if err != nil { 39 | logger.Logger.Panic(err) 40 | } 41 | for i, unit := range langCNData.Units { 42 | LangMap[unit.Id] = langCNData.Units[i].Content 43 | } 44 | 45 | var itemData pb.ItemData 46 | err = util.GetTableData(tableDir, &itemData) 47 | if err != nil { 48 | logger.Logger.Panic(err) 49 | } 50 | for i, item := range itemData.Units { 51 | ItemMap[item.Id] = itemData.Units[i] 52 | switch item.Type { 53 | case 10: 54 | DollNameMapping[LangMap[item.Name.Id]] = item.Id 55 | case 20: 56 | WeaponNameMapping[LangMap[item.Name.Id]] = item.Id 57 | } 58 | } 59 | 60 | var gachaData pb.GachaData 61 | err = util.GetTableData(tableDir, &gachaData) 62 | if err != nil { 63 | logger.Logger.Panic(err) 64 | } 65 | for _, unit := range gachaData.Units { 66 | //等级为5在卡池中不一定算5星,比如神秘箱中的导体5/6,因此需要从卡池道具列表中提取等级 67 | ItemRankMap[unit.Id] = make(map[int64]int64) 68 | //提取人形,散爆这里居然有中文冒号你敢信 69 | rateDesGun := strings.ReplaceAll(unit.RateDesGun, ":", ":") 70 | rateDollStringGroupList := strings.Split(rateDesGun, ",") 71 | for _, rateDollStringGroup := range rateDollStringGroupList { 72 | var rank int64 73 | var dollStringGroup string 74 | switch { 75 | case strings.HasPrefix(rateDollStringGroup, "5:"): 76 | rank = 5 77 | dollStringGroup = strings.TrimPrefix(rateDollStringGroup, "5:") 78 | case strings.HasPrefix(rateDollStringGroup, "4:"): 79 | rank = 4 80 | dollStringGroup = strings.TrimPrefix(rateDollStringGroup, "4:") 81 | case strings.HasPrefix(rateDollStringGroup, "3:"): 82 | rank = 3 83 | dollStringGroup = strings.TrimPrefix(rateDollStringGroup, "3:") 84 | default: 85 | if rateDollStringGroup != "" { 86 | logger.Logger.Errorf("未知的人形掉落列表:%s", rateDollStringGroup) 87 | } 88 | continue 89 | } 90 | dollStringList := strings.Split(dollStringGroup, ":") 91 | for _, dollString := range dollStringList { 92 | dollId, err := strconv.ParseInt(dollString, 10, 64) 93 | if err != nil { 94 | logger.Logger.Panic(err) 95 | } 96 | ItemRankMap[unit.Id][dollId] = rank 97 | } 98 | } 99 | //提取武器,散爆这里居然有中文冒号你敢信 100 | rateDesWeapon := strings.ReplaceAll(unit.RateDesWeapon, ":", ":") 101 | rateWeaponStringGroupList := strings.Split(rateDesWeapon, ",") 102 | for _, rateWeaponStringGroup := range rateWeaponStringGroupList { 103 | var rank int64 104 | var weaponStringGroup string 105 | switch { 106 | case strings.HasPrefix(rateWeaponStringGroup, "5:"): 107 | rank = 5 108 | weaponStringGroup = strings.TrimPrefix(rateWeaponStringGroup, "5:") 109 | case strings.HasPrefix(rateWeaponStringGroup, "4:"): 110 | rank = 4 111 | weaponStringGroup = strings.TrimPrefix(rateWeaponStringGroup, "4:") 112 | case strings.HasPrefix(rateWeaponStringGroup, "3:"): 113 | rank = 3 114 | weaponStringGroup = strings.TrimPrefix(rateWeaponStringGroup, "3:") 115 | default: 116 | if rateWeaponStringGroup != "" { 117 | logger.Logger.Errorf("未知的道具等级列表:%s", rateWeaponStringGroup) 118 | } 119 | continue 120 | } 121 | weaponStringList := strings.Split(weaponStringGroup, ":") 122 | for _, weaponString := range weaponStringList { 123 | weaponId, err := strconv.ParseInt(weaponString, 10, 64) 124 | if err != nil { 125 | logger.Logger.Panic(err) 126 | } 127 | ItemRankMap[unit.Id][weaponId] = rank 128 | } 129 | } 130 | 131 | //限定池需要提取upItem来分辨是否歪 132 | if unit.Type == 3 || unit.Type == 4 { 133 | var upItemGroupString string 134 | if unit.GunUpItem != "" { 135 | upItemGroupString = unit.GunUpItem 136 | } else { 137 | upItemGroupString = unit.WeaponUpItem 138 | } 139 | upItemGroupList := strings.Split(upItemGroupString, ",") 140 | for _, upItemGroup := range upItemGroupList { 141 | if strings.HasPrefix(upItemGroup, "5:") { 142 | upItemString := strings.TrimPrefix(upItemGroup, "5:") 143 | upItemId, err := strconv.ParseInt(upItemString, 10, 64) 144 | if err != nil { 145 | logger.Logger.Panic(err) 146 | } 147 | UpItemMap[unit.Id] = upItemId 148 | break 149 | } 150 | } 151 | } 152 | } 153 | 154 | var gachaTypeListData pb.GachaTypeListData 155 | err = util.GetTableData(tableDir, &gachaTypeListData) 156 | for i, unit := range gachaTypeListData.Units { 157 | PoolTypeMap[unit.Id] = gachaTypeListData.Units[i] 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /request/CommunityCommon.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/pkg/errors" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | ) 12 | 13 | type CommonResponse struct { 14 | Code int `json:"Code"` 15 | Message string `json:"Message"` 16 | Data json.RawMessage `json:"data"` 17 | } 18 | 19 | func (c CommonResponse) Error() string { 20 | return fmt.Sprintf("%s(Code:%d)", c.Message, c.Code) 21 | } 22 | 23 | func CommunityGet(apiUrl string, values url.Values, webToken string) (dataBytes json.RawMessage, err error) { 24 | apiUrl += "?" + values.Encode() 25 | request, err := http.NewRequest("GET", apiUrl, nil) 26 | if err != nil { 27 | return nil, errors.WithStack(err) 28 | } 29 | 30 | request.Header.Set("Content-Type", "application/json") 31 | request.Header.Set("Origin", "https://gf2-bbs.sunborngame.com") 32 | request.Header.Set("Referer", "https://gf2-bbs.sunborngame.com") 33 | request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0") 34 | if webToken != "" { 35 | request.Header.Set("Authorization", webToken) 36 | } 37 | 38 | resp, err := http.DefaultClient.Do(request) 39 | if err != nil { 40 | return nil, errors.WithStack(err) 41 | } 42 | defer resp.Body.Close() 43 | 44 | body, err := io.ReadAll(resp.Body) 45 | if err != nil { 46 | return nil, errors.WithStack(err) 47 | } 48 | var respBody CommonResponse 49 | err = json.Unmarshal(body, &respBody) 50 | if err != nil { 51 | return nil, errors.WithStack(err) 52 | } 53 | 54 | if respBody.Code != 0 { 55 | return nil, errors.WithStack(respBody) 56 | } 57 | 58 | return respBody.Data, nil 59 | } 60 | 61 | func CommunityPost(apiUrl string, params map[string]interface{}, webToken string) (dataBytes json.RawMessage, err error) { 62 | requestBodyBytes, err := json.Marshal(¶ms) 63 | if err != nil { 64 | return nil, errors.WithStack(err) 65 | } 66 | request, err := http.NewRequest("POST", apiUrl, bytes.NewReader(requestBodyBytes)) 67 | if err != nil { 68 | return nil, errors.WithStack(err) 69 | } 70 | 71 | request.Header.Set("Content-Type", "application/json") 72 | request.Header.Set("Origin", "https://gf2-bbs.sunborngame.com") 73 | request.Header.Set("Referer", "https://gf2-bbs.sunborngame.com") 74 | request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0") 75 | if webToken != "" { 76 | request.Header.Set("Authorization", webToken) 77 | } 78 | 79 | resp, err := http.DefaultClient.Do(request) 80 | if err != nil { 81 | return nil, errors.WithStack(err) 82 | } 83 | defer resp.Body.Close() 84 | 85 | body, err := io.ReadAll(resp.Body) 86 | if err != nil { 87 | return nil, errors.WithStack(err) 88 | } 89 | var respBody CommonResponse 90 | err = json.Unmarshal(body, &respBody) 91 | if err != nil { 92 | return nil, errors.WithStack(err) 93 | } 94 | 95 | if respBody.Code != 0 { 96 | return nil, errors.WithStack(respBody) 97 | } 98 | 99 | return respBody.Data, nil 100 | } 101 | -------------------------------------------------------------------------------- /request/CommunityExchange.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | func CommunityExchange(webToken string, exchangeId int64) error { 8 | apiUrl := `https://gf2-bbs-api.sunborngame.com/community/item/exchange` 9 | 10 | params := map[string]interface{}{ 11 | "exchange_id": exchangeId, 12 | } 13 | 14 | _, err := CommunityPost(apiUrl, params, webToken) 15 | if err != nil { 16 | return errors.WithStack(err) 17 | } 18 | 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /request/CommunityExchangeList.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | type CommunityExchangeListData struct { 9 | List []struct { 10 | ExchangeId int64 `json:"exchange_id"` 11 | ItemName string `json:"item_name"` 12 | ItemCount int64 `json:"item_count"` 13 | ItemPic string `json:"item_pic"` 14 | ItemContext string `json:"item_context"` 15 | UseScore int64 `json:"use_score"` 16 | ExchangeCount int64 `json:"exchange_count"` 17 | MaxExchangeCount int64 `json:"max_exchange_count"` 18 | Cycle string `json:"cycle"` 19 | } `json:"list"` 20 | Total int64 `json:"total"` 21 | } 22 | 23 | func CommunityExchangeList(webToken string) (CommunityExchangeListData, error) { 24 | apiUrl := `https://gf2-bbs-api.sunborngame.com/community/item/exchange_list` 25 | 26 | dataBytes, err := CommunityGet(apiUrl, nil, webToken) 27 | if err != nil { 28 | return CommunityExchangeListData{}, errors.WithStack(err) 29 | } 30 | 31 | var data CommunityExchangeListData 32 | err = json.Unmarshal(dataBytes, &data) 33 | if err != nil { 34 | return CommunityExchangeListData{}, errors.WithStack(err) 35 | } 36 | 37 | return data, nil 38 | } 39 | -------------------------------------------------------------------------------- /request/CommunityLogin.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | type CommunityLoginData struct { 9 | Account struct { 10 | Token string `json:"token"` 11 | Uid int64 `json:"uid"` 12 | PlatformId int64 `json:"platform_id"` 13 | ChannelId int64 `json:"channel_id"` 14 | } `json:"account"` 15 | } 16 | 17 | func CommunityLogin(gameToken string) (webToken string, err error) { 18 | apiUrl := `https://gf2-bbs-api.sunborngame.com/login/game_skip` 19 | params := map[string]interface{}{ 20 | "game_token": gameToken, 21 | } 22 | 23 | dataBytes, err := CommunityPost(apiUrl, params, "") 24 | if err != nil { 25 | return "", errors.WithStack(err) 26 | } 27 | 28 | var data CommunityLoginData 29 | err = json.Unmarshal(dataBytes, &data) 30 | if err != nil { 31 | return "", errors.WithStack(err) 32 | } 33 | 34 | return data.Account.Token, nil 35 | } 36 | -------------------------------------------------------------------------------- /request/CommunitySign.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | type CommunitySignData struct { 9 | GetExp int `json:"get_exp"` 10 | GetItemCount int `json:"get_item_count"` 11 | GetItemName string `json:"get_item_name"` 12 | GetItemUrl string `json:"get_item_url"` 13 | GetScore int `json:"get_score"` 14 | } 15 | 16 | func CommunitySign(webToken string) (CommunitySignData, error) { 17 | apiUrl := `https://gf2-bbs-api.sunborngame.com/community/task/sign_in` 18 | 19 | dataBytes, err := CommunityPost(apiUrl, nil, webToken) 20 | if err != nil { 21 | return CommunitySignData{}, errors.WithStack(err) 22 | } 23 | 24 | var data CommunitySignData 25 | err = json.Unmarshal(dataBytes, &data) 26 | if err != nil { 27 | return CommunitySignData{}, errors.WithStack(err) 28 | } 29 | 30 | return data, nil 31 | } 32 | -------------------------------------------------------------------------------- /request/CommunityTaskList.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | type CommunityTaskListData struct { 9 | DailyTask []struct { 10 | TaskName string `json:"task_name"` 11 | TaskContext string `json:"task_context"` 12 | CompleteCount int64 `json:"complete_count"` 13 | MaxCompleteCount int64 `json:"max_complete_count"` 14 | } `json:"daily_task"` 15 | MoreTask []struct { 16 | TaskName string `json:"task_name"` 17 | TaskContext string `json:"task_context"` 18 | CompleteCount int64 `json:"complete_count"` 19 | MaxCompleteCount int64 `json:"max_complete_count"` 20 | } `json:"more_task"` 21 | } 22 | 23 | func CommunityTaskList(webToken string) (CommunityTaskListData, error) { 24 | apiUrl := `https://gf2-bbs-api.sunborngame.com/community/task/get_current_task_list` 25 | 26 | dataBytes, err := CommunityGet(apiUrl, nil, webToken) 27 | if err != nil { 28 | return CommunityTaskListData{}, errors.WithStack(err) 29 | } 30 | 31 | var data CommunityTaskListData 32 | err = json.Unmarshal(dataBytes, &data) 33 | if err != nil { 34 | return CommunityTaskListData{}, errors.WithStack(err) 35 | } 36 | 37 | return data, nil 38 | } 39 | -------------------------------------------------------------------------------- /request/CommunityTopicLike.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "net/url" 7 | "strconv" 8 | ) 9 | 10 | func CommunityTopicLike(webToken string, topicId int64) error { 11 | apiUrl := fmt.Sprintf(`https://gf2-bbs-api.sunborngame.com/community/topic/like/%d`, topicId) 12 | 13 | values := url.Values{ 14 | "id": []string{strconv.FormatInt(topicId, 10)}, 15 | } 16 | 17 | _, err := CommunityGet(apiUrl, values, webToken) 18 | if err != nil { 19 | return errors.WithStack(err) 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /request/CommunityTopicList.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/pkg/errors" 6 | "net/url" 7 | "strconv" 8 | ) 9 | 10 | type CommunityTopicListData struct { 11 | LastTid int64 `json:"last_tid"` 12 | List []struct { 13 | CategoryId int64 `json:"category_id"` 14 | CategoryName string `json:"category_name"` 15 | CommentNum interface{} `json:"comment_num"` 16 | Content string `json:"content"` 17 | CreateTime string `json:"create_time"` 18 | IsAdmin bool `json:"is_admin"` 19 | IsAuthor bool `json:"is_author"` 20 | IsFavor bool `json:"is_favor"` 21 | IsLike bool `json:"is_like"` 22 | LikeNum interface{} `json:"like_num"` 23 | PicList []string `json:"pic_list"` 24 | Title string `json:"title"` 25 | TopicId int64 `json:"topic_id"` 26 | UserAvatar string `json:"user_avatar"` 27 | UserId int64 `json:"user_id"` 28 | UserLevel int64 `json:"user_level"` 29 | UserNickName string `json:"user_nick_name"` 30 | } `json:"list"` 31 | NextPage bool `json:"next_page"` 32 | PubTime int64 `json:"pub_time"` 33 | Total int64 `json:"total"` 34 | } 35 | 36 | func CommunityTopicList(webToken string, userId int64) (CommunityTopicListData, error) { 37 | apiUrl := `https://gf2-bbs-api.sunborngame.com/community/topic/list` 38 | 39 | values := url.Values{ 40 | "last_tid": []string{"0"}, 41 | "pub_time": []string{"0"}, 42 | "reply_time": []string{"0"}, 43 | "hot_value": []string{"0"}, 44 | } 45 | if userId > 0 { 46 | values.Set("sort_type", "1") 47 | values.Set("category_id", "100") 48 | values.Set("query_type", "4") 49 | values.Set("user_id", strconv.FormatInt(userId, 10)) 50 | } else { 51 | values.Set("sort_type", "2") 52 | values.Set("category_id", "5") 53 | values.Set("query_type", "1") 54 | } 55 | 56 | dataBytes, err := CommunityGet(apiUrl, values, webToken) 57 | if err != nil { 58 | return CommunityTopicListData{}, errors.WithStack(err) 59 | } 60 | 61 | var data CommunityTopicListData 62 | err = json.Unmarshal(dataBytes, &data) 63 | if err != nil { 64 | return CommunityTopicListData{}, errors.WithStack(err) 65 | } 66 | 67 | return data, nil 68 | } 69 | -------------------------------------------------------------------------------- /request/CommunityTopicShare.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "net/url" 7 | "strconv" 8 | ) 9 | 10 | func CommunityTopicShare(webToken string, topicId int64) error { 11 | apiUrl := fmt.Sprintf(`https://gf2-bbs-api.sunborngame.com/community/topic/share/%d`, topicId) 12 | 13 | values := url.Values{ 14 | "id": []string{strconv.FormatInt(topicId, 10)}, 15 | } 16 | 17 | _, err := CommunityGet(apiUrl, values, webToken) 18 | if err != nil { 19 | return errors.WithStack(err) 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /request/CommunityTopicView.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/pkg/errors" 7 | "net/url" 8 | "strconv" 9 | ) 10 | 11 | type CommunityTopicViewData struct { 12 | BadNum interface{} `json:"bad_num"` 13 | CategoryId int64 `json:"category_id"` 14 | CategoryName string `json:"category_name"` 15 | CommentNum interface{} `json:"comment_num"` 16 | Content string `json:"content"` 17 | CreateTime string `json:"create_time"` 18 | IpLocation string `json:"ip_location"` 19 | IsAdmin bool `json:"is_admin"` 20 | IsAuthor bool `json:"is_author"` 21 | IsBad bool `json:"is_bad"` 22 | IsFavor bool `json:"is_favor"` 23 | IsFollow bool `json:"is_follow"` 24 | IsLike bool `json:"is_like"` 25 | LikeNum interface{} `json:"like_num"` 26 | LikeUserAvatars []string `json:"like_user_avatars"` 27 | PicList []string `json:"pic_list"` 28 | Title string `json:"title"` 29 | TopicId int64 `json:"topic_id"` 30 | UpdateTime string `json:"update_time"` 31 | UserAvatar string `json:"user_avatar"` 32 | UserId int64 `json:"user_id"` 33 | UserLevel int64 `json:"user_level"` 34 | UserNickName string `json:"user_nick_name"` 35 | ViewNum interface{} `json:"view_num"` 36 | } 37 | 38 | func CommunityTopicView(webToken string, topicId int64) (CommunityTopicViewData, error) { 39 | apiUrl := fmt.Sprintf(`https://gf2-bbs-api.sunborngame.com/community/topic/%d`, topicId) 40 | 41 | values := url.Values{ 42 | "id": []string{strconv.FormatInt(topicId, 10)}, 43 | } 44 | 45 | dataBytes, err := CommunityGet(apiUrl, values, webToken) 46 | if err != nil { 47 | return CommunityTopicViewData{}, errors.WithStack(err) 48 | } 49 | 50 | var data CommunityTopicViewData 51 | err = json.Unmarshal(dataBytes, &data) 52 | if err != nil { 53 | return CommunityTopicViewData{}, errors.WithStack(err) 54 | } 55 | 56 | return data, nil 57 | } 58 | -------------------------------------------------------------------------------- /request/CommunityUserInfo.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | type CommunityUserInfoData struct { 9 | User struct { 10 | AuthLock int64 `json:"auth_lock"` 11 | AuthType int64 `json:"auth_type"` 12 | Avatar string `json:"avatar"` 13 | Exp int64 `json:"exp"` 14 | Fans interface{} `json:"fans"` 15 | Favors interface{} `json:"favors"` 16 | Follows interface{} `json:"follows"` 17 | GameCommanderLevel int64 `json:"game_commander_level"` 18 | GameNickName string `json:"game_nick_name"` 19 | GameUid int64 `json:"game_uid"` 20 | IpLocation string `json:"ip_location"` 21 | IsAdmin bool `json:"is_admin"` 22 | IsAuthor bool `json:"is_author"` 23 | IsFollow bool `json:"is_follow"` 24 | Level int64 `json:"level"` 25 | Likes interface{} `json:"likes"` 26 | NextLvExp int64 `json:"next_lv_exp"` 27 | NickName string `json:"nick_name"` 28 | Score int64 `json:"score"` 29 | Signature string `json:"signature"` 30 | Uid int64 `json:"uid"` 31 | } `json:"user"` 32 | } 33 | 34 | func CommunityUserInfo(webToken string) (CommunityUserInfoData, error) { 35 | apiUrl := `https://gf2-bbs-api.sunborngame.com/community/member/info` 36 | params := map[string]interface{}{ 37 | "uid": 0, 38 | } 39 | 40 | dataBytes, err := CommunityPost(apiUrl, params, webToken) 41 | if err != nil { 42 | return CommunityUserInfoData{}, errors.WithStack(err) 43 | } 44 | 45 | var data CommunityUserInfoData 46 | err = json.Unmarshal(dataBytes, &data) 47 | if err != nil { 48 | return CommunityUserInfoData{}, errors.WithStack(err) 49 | } 50 | 51 | return data, nil 52 | } 53 | -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatchaCabin/gf2gacha/98957835ff3b0b90afa5af33d84bb44ba4e4f34d/sample.png -------------------------------------------------------------------------------- /util/BackupDB.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "os" 7 | "time" 8 | ) 9 | 10 | func BackupDB() error { 11 | dbData, err := os.ReadFile("gf2gacha.db") 12 | if err != nil { 13 | return errors.WithStack(err) 14 | } 15 | 16 | backupName := fmt.Sprintf("gf2gacha_backup%s.db", time.Now().Format("20060102150405")) 17 | err = os.WriteFile(backupName, dbData, 0755) 18 | if err != nil { 19 | return errors.WithStack(err) 20 | } 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /util/Cert.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | "fmt" 12 | "github.com/pkg/errors" 13 | "golang.org/x/sys/windows" 14 | "math/big" 15 | "os" 16 | "syscall" 17 | "time" 18 | "unsafe" 19 | ) 20 | 21 | func GenCA() error { 22 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 23 | if err != nil { 24 | return err 25 | } 26 | serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | template := x509.Certificate{ 32 | SerialNumber: serialNumber, 33 | Subject: pkix.Name{ 34 | CommonName: "GF2Gacha Custom CA", 35 | Organization: []string{"GF2Gacha Custom Org"}, 36 | Country: []string{"CN"}, 37 | }, 38 | NotBefore: time.Now(), 39 | NotAfter: time.Now().AddDate(20, 0, 0), 40 | KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 41 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 42 | BasicConstraintsValid: true, 43 | IsCA: true, 44 | } 45 | 46 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) 47 | if err != nil { 48 | return err 49 | } 50 | certOut, err := os.Create("ca.crt") 51 | if err != nil { 52 | return err 53 | } 54 | defer certOut.Close() 55 | err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | keyOut, err := os.Create("ca.key") 61 | if err != nil { 62 | return err 63 | } 64 | defer keyOut.Close() 65 | privBytes, err := x509.MarshalECPrivateKey(privateKey) 66 | if err != nil { 67 | return err 68 | } 69 | err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes}) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | return nil 75 | } 76 | 77 | var ( 78 | crypt32 = syscall.NewLazyDLL("crypt32.dll") 79 | procCertOpenStore = crypt32.NewProc("CertOpenStore") 80 | procCertCloseStore = crypt32.NewProc("CertCloseStore") 81 | procCertAddEncodedCertificateToStore = crypt32.NewProc("CertAddEncodedCertificateToStore") 82 | ) 83 | 84 | func IsTrustedCA() bool { 85 | certPem, err := os.ReadFile("ca.crt") 86 | if err != nil { 87 | fmt.Println(err) 88 | return false 89 | } 90 | certDer, _ := pem.Decode(certPem) 91 | if certDer == nil { 92 | fmt.Println("pem解码失败") 93 | return false 94 | } 95 | cert, err := x509.ParseCertificate(certDer.Bytes) 96 | if err != nil { 97 | fmt.Println(err) 98 | return false 99 | } 100 | 101 | _, err = cert.Verify(x509.VerifyOptions{}) 102 | if err != nil { 103 | fmt.Println(err) 104 | return false 105 | } 106 | 107 | return true 108 | } 109 | 110 | func openCertStore() (windows.Handle, error) { 111 | storeName, _ := syscall.UTF16PtrFromString("Root") 112 | 113 | r, _, err := procCertOpenStore.Call(uintptr(10), 0, 0, uintptr(0x00010000), uintptr(unsafe.Pointer(storeName))) 114 | if r == 0 { 115 | return 0, fmt.Errorf("调用CertOpenStore出错: %v", err) 116 | } 117 | return windows.Handle(r), nil 118 | } 119 | 120 | func closeCertStore(store windows.Handle) { 121 | if store != 0 { 122 | procCertCloseStore.Call(uintptr(store), 0) 123 | } 124 | } 125 | 126 | func InstallCA() error { 127 | certPem, err := os.ReadFile("ca.crt") 128 | if err != nil { 129 | return err 130 | } 131 | certDer, _ := pem.Decode(certPem) 132 | if certDer == nil { 133 | return errors.New("pem解码失败") 134 | } 135 | 136 | store, err := openCertStore() 137 | if err != nil { 138 | return err 139 | } 140 | defer closeCertStore(store) 141 | 142 | r, _, err := procCertAddEncodedCertificateToStore.Call( 143 | uintptr(store), 144 | uintptr(windows.X509_ASN_ENCODING), 145 | uintptr(unsafe.Pointer(&certDer.Bytes[0])), 146 | uintptr(len(certDer.Bytes)), 147 | uintptr(4), 148 | 0, 149 | ) 150 | 151 | if r == 0 { 152 | return fmt.Errorf("调用CertAddEncodedCertificateToStore出错: %v", err) 153 | } 154 | return nil 155 | } 156 | 157 | func ParseCA() (*tls.Certificate, error) { 158 | caCert, err := os.ReadFile("ca.crt") 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | caKey, err := os.ReadFile("ca.key") 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | parsedCert, err := tls.X509KeyPair(caCert, caKey) 169 | if err != nil { 170 | return nil, err 171 | } 172 | if parsedCert.Leaf, err = x509.ParseCertificate(parsedCert.Certificate[0]); err != nil { 173 | return nil, err 174 | } 175 | return &parsedCert, nil 176 | } 177 | -------------------------------------------------------------------------------- /util/GetGameDataDir.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "gf2gacha/config" 5 | "gf2gacha/logger" 6 | "github.com/pkg/errors" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | ) 11 | 12 | func GetGameDataDir() (string, error) { 13 | userHome, err := os.UserHomeDir() 14 | if err != nil { 15 | return "", errors.WithStack(err) 16 | } 17 | 18 | logPath := filepath.Join(userHome, "/AppData/LocalLow/SunBorn/少女前线2:追放/Player.log") 19 | logData, err := os.ReadFile(logPath) 20 | if err != nil { 21 | return "", errors.WithStack(err) 22 | } 23 | 24 | regexpGamePath, err := regexp.Compile(`\[Subsystems] Discovering subsystems at path (.+)/UnitySubsystems`) 25 | if err != nil { 26 | return "", errors.WithStack(err) 27 | } 28 | resultGamePath := regexpGamePath.FindSubmatch(logData) 29 | if len(resultGamePath) == 2 { 30 | gameDataDir := filepath.Join(string(resultGamePath[1]), "LocalCache/Data") 31 | 32 | //保存游戏路径,供日志里找不到时降级使用 33 | err = config.SetGameDataDir(gameDataDir) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | return gameDataDir, nil 39 | } 40 | 41 | logger.Logger.Warn("未在日志中找到游戏路径,尝试从配置文件读取") 42 | //如果日志里没找到游戏路径,尝试降级读取配置文件 43 | if !config.IsSetGameDataDir() { 44 | return "", errors.New("日志和配置文件中均未找到游戏路径") 45 | } 46 | 47 | return config.GetGameDataDir(), nil 48 | } 49 | -------------------------------------------------------------------------------- /util/GetLogInfo.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "gf2gacha/model" 5 | "github.com/pkg/errors" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | ) 10 | 11 | func GetLogInfo() (logInfo model.LogInfo, err error) { 12 | userHome, err := os.UserHomeDir() 13 | if err != nil { 14 | return model.LogInfo{}, errors.WithStack(err) 15 | } 16 | 17 | logPath := filepath.Join(userHome, "/AppData/LocalLow/SunBorn/少女前线2:追放/Player.log") 18 | logData, err := os.ReadFile(logPath) 19 | if err != nil { 20 | return model.LogInfo{}, errors.WithStack(err) 21 | } 22 | 23 | regexpGamePath, err := regexp.Compile(`\[Subsystems] Discovering subsystems at path (.+)/UnitySubsystems`) 24 | if err != nil { 25 | return model.LogInfo{}, errors.WithStack(err) 26 | } 27 | resultGamePath := regexpGamePath.FindSubmatch(logData) 28 | if len(resultGamePath) == 2 { 29 | logInfo.TablePath = filepath.Join(string(resultGamePath[1]), "LocalCache/Data/Table") 30 | } else { 31 | return model.LogInfo{}, errors.New("未在日志中找到游戏路径") 32 | } 33 | 34 | regexpUserInfo, err := regexp.Compile(`"access_token":"(.+?)".+"uid":(\d+)`) 35 | if err != nil { 36 | return model.LogInfo{}, errors.WithStack(err) 37 | } 38 | resultUserInfoList := regexpUserInfo.FindAllSubmatch(logData, -1) 39 | if len(resultUserInfoList) == 0 { 40 | return model.LogInfo{}, errors.New("未在日志中找到AccessToken或Uid,可能是最近一次游戏启动时未登录,官服请使用「捕获信息」按钮抓包") 41 | } 42 | resultUserInfo := resultUserInfoList[len(resultUserInfoList)-1] 43 | if len(resultUserInfo) == 3 { 44 | logInfo.AccessToken = string(resultUserInfo[1]) 45 | logInfo.Uid = string(resultUserInfo[2]) 46 | } else { 47 | return model.LogInfo{}, errors.New("未在日志中找到AccessToken或Uid,可能是最近一次游戏启动时未登录,官服请使用「捕获信息」按钮抓包") 48 | } 49 | 50 | regexpGachaUrl, err := regexp.Compile(`"gacha_record_url":"(.*?)"`) 51 | if err != nil { 52 | return model.LogInfo{}, errors.WithStack(err) 53 | } 54 | resultGachaUrlList := regexpGachaUrl.FindAllSubmatch(logData, -1) 55 | if len(resultGachaUrlList) == 0 { 56 | return model.LogInfo{}, errors.New("未在日志中找到抽卡链接,官服请使用「捕获信息」按钮抓包") 57 | } 58 | resultGachaUrl := resultGachaUrlList[len(resultGachaUrlList)-1] 59 | if len(resultGachaUrl) == 2 { 60 | logInfo.GachaUrl = string(resultGachaUrl[1]) 61 | } else { 62 | return model.LogInfo{}, errors.New("未在日志中找到抽卡链接") 63 | } 64 | 65 | return logInfo, nil 66 | } 67 | -------------------------------------------------------------------------------- /util/GetTableData.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "github.com/pkg/errors" 7 | "google.golang.org/protobuf/proto" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | ) 12 | 13 | func GetTableData[T proto.Message](tablePath string, tableData T) error { 14 | tableType := reflect.TypeOf(tableData) 15 | 16 | fileName := tableType.Elem().Name() + ".bytes" 17 | fileData, err := os.ReadFile(filepath.Join(tablePath, fileName)) 18 | if err != nil { 19 | return errors.WithStack(err) 20 | } 21 | 22 | var head uint32 23 | err = binary.Read(bytes.NewReader(fileData[:4]), binary.LittleEndian, &head) 24 | if err != nil { 25 | return errors.WithStack(err) 26 | } 27 | 28 | err = proto.Unmarshal(fileData[head+4:], tableData) 29 | if err != nil { 30 | return errors.WithStack(err) 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /util/SysProxy.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/sys/windows/registry" 6 | ) 7 | 8 | var ( 9 | originProxyServer string 10 | originProxyEnable uint32 11 | ) 12 | 13 | func EnableSysProxy(port int) error { 14 | key, err := registry.OpenKey( 15 | registry.CURRENT_USER, 16 | `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, 17 | registry.ALL_ACCESS, 18 | ) 19 | if err != nil { 20 | return err 21 | } 22 | defer key.Close() 23 | 24 | proxyServer, _, err := key.GetStringValue("ProxyServer") 25 | if err != nil { 26 | return err 27 | } 28 | originProxyServer = proxyServer 29 | 30 | if err := key.SetStringValue("ProxyServer", fmt.Sprintf("127.0.0.1:%d", port)); err != nil { 31 | return err 32 | } 33 | 34 | proxyEnable, _, err := key.GetIntegerValue("ProxyEnable") 35 | if err != nil { 36 | return err 37 | } 38 | originProxyEnable = uint32(proxyEnable) 39 | 40 | if err := key.SetDWordValue("ProxyEnable", 1); err != nil { 41 | return err 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func DisableSysProxy() error { 48 | key, err := registry.OpenKey( 49 | registry.CURRENT_USER, 50 | `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, 51 | registry.ALL_ACCESS, 52 | ) 53 | if err != nil { 54 | return err 55 | } 56 | defer key.Close() 57 | 58 | if err := key.SetStringValue("ProxyServer", originProxyServer); err != nil { 59 | return err 60 | } 61 | 62 | if err := key.SetDWordValue("ProxyEnable", originProxyEnable); err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /util/Version.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | const version = "" 4 | 5 | func GetVersion() string { 6 | return version 7 | } 8 | -------------------------------------------------------------------------------- /wails.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://wails.io/schemas/config.v2.json", 3 | "name": "gf2gacha", 4 | "outputfilename": "gf2gacha", 5 | "frontend:install": "npm install", 6 | "frontend:build": "npm run build", 7 | "frontend:dev:watcher": "npm run dev", 8 | "frontend:dev:serverUrl": "auto", 9 | "author": { 10 | "name": "Matcha", 11 | "email": "admin@mcc.wiki" 12 | }, 13 | "info": { 14 | "companyName": "MccWiki", 15 | "productName": "gf2gacha", 16 | "productVersion": "", 17 | "copyright": "Copyright 2024 MccWiki", 18 | "comments": "A tool for storing and analyzing GFL2 gacha records" 19 | } 20 | } 21 | --------------------------------------------------------------------------------